@@ -8,15 +8,18 @@ import { isProviderApiKeyConfigured } from "openclaw/plugin-sdk/provider-auth";
88import { resolveApiKeyForProvider } from "openclaw/plugin-sdk/provider-auth-runtime" ;
99import {
1010 assertOkOrThrowHttpError ,
11+ createProviderOperationDeadline ,
1112 fetchProviderDownloadResponse ,
1213 postJsonRequest ,
14+ resolveProviderOperationTimeoutMs ,
1315 resolveProviderHttpRequestConfig ,
1416} from "openclaw/plugin-sdk/provider-http" ;
1517import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime" ;
1618
1719const DEFAULT_MINIMAX_MUSIC_BASE_URL = "https://api.minimax.io" ;
1820const DEFAULT_MINIMAX_MUSIC_MODEL = "music-2.6" ;
1921const DEFAULT_TIMEOUT_MS = 120_000 ;
22+ const DEFAULT_OPERATION_TIMEOUT_MS = 300_000 ;
2023
2124type MinimaxBaseResp = {
2225 status_code ?: number ;
@@ -36,6 +39,14 @@ type MinimaxMusicCreateResponse = {
3639 base_resp ?: MinimaxBaseResp ;
3740} ;
3841
42+ type MinimaxMusicStreamFrame = {
43+ data ?: {
44+ audio ?: string ;
45+ status ?: number | string ;
46+ } ;
47+ base_resp ?: MinimaxBaseResp ;
48+ } ;
49+
3950function resolveMinimaxMusicBaseUrl (
4051 cfg : Parameters < typeof resolveApiKeyForProvider > [ 0 ] [ "cfg" ] ,
4152 providerId : string ,
@@ -106,6 +117,48 @@ async function downloadTrackFromUrl(params: {
106117 } ;
107118}
108119
120+ async function readStreamingTrack ( response : Response ) : Promise < GeneratedMusicAsset > {
121+ const contentType = normalizeOptionalString ( response . headers . get ( "content-type" ) ) ?? "" ;
122+ if ( contentType . toLowerCase ( ) . startsWith ( "audio/" ) ) {
123+ const ext = extensionForMime ( contentType ) ?. replace ( / ^ \. / u, "" ) || "mp3" ;
124+ return {
125+ buffer : Buffer . from ( await response . arrayBuffer ( ) ) ,
126+ mimeType : contentType ,
127+ fileName : `track-1.${ ext } ` ,
128+ } ;
129+ }
130+ const chunks : Buffer [ ] = [ ] ;
131+ const text = await response . text ( ) ;
132+ for ( const rawLine of text . split ( / \r ? \n / u) ) {
133+ const line = rawLine . trim ( ) ;
134+ if ( ! line . startsWith ( "data:" ) ) {
135+ continue ;
136+ }
137+ const json = line . slice ( "data:" . length ) . trim ( ) ;
138+ if ( ! json || json === "[DONE]" ) {
139+ continue ;
140+ }
141+ const frame = JSON . parse ( json ) as MinimaxMusicStreamFrame ;
142+ assertMinimaxBaseResp ( frame . base_resp , "MiniMax music generation failed" ) ;
143+ const audio = normalizeOptionalString ( frame . data ?. audio ) ;
144+ if ( audio ) {
145+ if ( String ( frame . data ?. status ?? "" ) === "2" && chunks . length > 0 ) {
146+ continue ;
147+ }
148+ chunks . push ( decodePossibleBinary ( audio ) ) ;
149+ }
150+ }
151+ const buffer = Buffer . concat ( chunks ) ;
152+ if ( buffer . byteLength === 0 ) {
153+ throw new Error ( "MiniMax music generation response missing audio output" ) ;
154+ }
155+ return {
156+ buffer,
157+ mimeType : "audio/mpeg" ,
158+ fileName : "track-1.mp3" ,
159+ } ;
160+ }
161+
109162function buildPrompt ( req : MusicGenerationRequest ) : string {
110163 const parts = [ req . prompt . trim ( ) ] ;
111164 if ( typeof req . durationSeconds === "number" && Number . isFinite ( req . durationSeconds ) ) {
@@ -168,6 +221,10 @@ function buildMinimaxMusicProvider(providerId: string): MusicGenerationProvider
168221 }
169222
170223 const fetchFn = fetch ;
224+ const deadline = createProviderOperationDeadline ( {
225+ timeoutMs : req . timeoutMs ?? DEFAULT_OPERATION_TIMEOUT_MS ,
226+ label : "MiniMax music generation" ,
227+ } ) ;
171228 const { baseUrl, allowPrivateNetwork, headers, dispatcherPolicy } =
172229 resolveProviderHttpRequestConfig ( {
173230 baseUrl : resolveMinimaxMusicBaseUrl ( req . cfg , providerId ) ,
@@ -190,7 +247,8 @@ function buildMinimaxMusicProvider(providerId: string): MusicGenerationProvider
190247 prompt : buildPrompt ( req ) ,
191248 ...( req . instrumental === true ? { is_instrumental : true } : { } ) ,
192249 ...( lyrics ? { lyrics } : req . instrumental === true ? { } : { lyrics_optimizer : true } ) ,
193- output_format : "url" ,
250+ stream : true ,
251+ output_format : "hex" ,
194252 audio_setting : {
195253 sample_rate : 44_100 ,
196254 bitrate : 256_000 ,
@@ -202,7 +260,10 @@ function buildMinimaxMusicProvider(providerId: string): MusicGenerationProvider
202260 url : `${ baseUrl } /v1/music_generation` ,
203261 headers : jsonHeaders ,
204262 body,
205- timeoutMs : req . timeoutMs ?? DEFAULT_TIMEOUT_MS ,
263+ timeoutMs : resolveProviderOperationTimeoutMs ( {
264+ deadline,
265+ defaultTimeoutMs : DEFAULT_OPERATION_TIMEOUT_MS ,
266+ } ) ,
206267 fetchFn,
207268 pinDns : false ,
208269 allowPrivateNetwork,
@@ -211,22 +272,32 @@ function buildMinimaxMusicProvider(providerId: string): MusicGenerationProvider
211272
212273 try {
213274 await assertOkOrThrowHttpError ( res , "MiniMax music generation failed" ) ;
214- const payload = ( await res . json ( ) ) as MinimaxMusicCreateResponse ;
215- assertMinimaxBaseResp ( payload . base_resp , "MiniMax music generation failed" ) ;
275+ const contentType = normalizeOptionalString ( res . headers . get ( "content-type" ) ) ?? "" ;
276+ const lowerContentType = contentType . toLowerCase ( ) ;
277+ const payload =
278+ lowerContentType . includes ( "text/event-stream" ) || lowerContentType . startsWith ( "audio/" )
279+ ? null
280+ : ( ( await res . clone ( ) . json ( ) ) as MinimaxMusicCreateResponse ) ;
281+ if ( payload ) {
282+ assertMinimaxBaseResp ( payload . base_resp , "MiniMax music generation failed" ) ;
283+ }
216284
217285 const audioCandidate =
218- normalizeOptionalString ( payload . audio ) ?? normalizeOptionalString ( payload . data ?. audio ) ;
286+ normalizeOptionalString ( payload ? .audio ) ?? normalizeOptionalString ( payload ? .data ?. audio ) ;
219287 const audioUrl =
220- normalizeOptionalString ( payload . audio_url ) ||
221- normalizeOptionalString ( payload . data ?. audio_url ) ||
288+ normalizeOptionalString ( payload ? .audio_url ) ||
289+ normalizeOptionalString ( payload ? .data ?. audio_url ) ||
222290 ( isLikelyRemoteUrl ( audioCandidate ) ? audioCandidate : undefined ) ;
223291 const inlineAudio = isLikelyRemoteUrl ( audioCandidate ) ? undefined : audioCandidate ;
224- const lyrics = decodePossibleText ( payload . lyrics ?? payload . data ?. lyrics ?? "" ) ;
292+ const lyrics = decodePossibleText ( payload ? .lyrics ?? payload ? .data ?. lyrics ?? "" ) ;
225293
226294 const track = audioUrl
227295 ? await downloadTrackFromUrl ( {
228296 url : audioUrl ,
229- timeoutMs : req . timeoutMs ,
297+ timeoutMs : resolveProviderOperationTimeoutMs ( {
298+ deadline,
299+ defaultTimeoutMs : DEFAULT_TIMEOUT_MS ,
300+ } ) ,
230301 fetchFn,
231302 } )
232303 : inlineAudio
@@ -235,7 +306,7 @@ function buildMinimaxMusicProvider(providerId: string): MusicGenerationProvider
235306 mimeType : "audio/mpeg" ,
236307 fileName : "track-1.mp3" ,
237308 }
238- : null ;
309+ : await readStreamingTrack ( res ) ;
239310 if ( ! track ) {
240311 throw new Error ( "MiniMax music generation response missing audio output" ) ;
241312 }
@@ -245,8 +316,8 @@ function buildMinimaxMusicProvider(providerId: string): MusicGenerationProvider
245316 ...( lyrics ? { lyrics : [ lyrics ] } : { } ) ,
246317 model,
247318 metadata : {
248- ...( normalizeOptionalString ( payload . task_id )
249- ? { taskId : normalizeOptionalString ( payload . task_id ) }
319+ ...( normalizeOptionalString ( payload ? .task_id )
320+ ? { taskId : normalizeOptionalString ( payload ? .task_id ) }
250321 : { } ) ,
251322 ...( audioUrl ? { audioUrl } : { } ) ,
252323 instrumental : req . instrumental === true ,
0 commit comments