@xiaozhiclaw/provider-core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/aliyun-oss-file-upload-adapter.d.ts +44 -0
- package/dist/adapters/aliyun-oss-file-upload-adapter.js +96 -0
- package/dist/adapters/gemini-file-upload-adapter.d.ts +26 -0
- package/dist/adapters/gemini-file-upload-adapter.js +92 -0
- package/dist/adapters/hub-oss-file-upload-adapter.d.ts +29 -0
- package/dist/adapters/hub-oss-file-upload-adapter.js +53 -0
- package/dist/adapters/index.d.ts +10 -0
- package/dist/adapters/index.js +10 -0
- package/dist/adapters/openai-file-upload-adapter.d.ts +38 -0
- package/dist/adapters/openai-file-upload-adapter.js +56 -0
- package/dist/adapters/volcengine-file-upload-adapter.d.ts +24 -0
- package/dist/adapters/volcengine-file-upload-adapter.js +45 -0
- package/dist/builtin-providers.d.ts +8 -0
- package/dist/builtin-providers.js +2237 -0
- package/dist/constants.d.ts +1 -0
- package/dist/constants.js +1 -0
- package/dist/credentials.d.ts +1 -0
- package/dist/credentials.js +8 -0
- package/dist/debug-transport.d.ts +12 -0
- package/dist/debug-transport.js +99 -0
- package/dist/errors.d.ts +11 -0
- package/dist/errors.js +12 -0
- package/dist/events.d.ts +48 -0
- package/dist/events.js +1 -0
- package/dist/file-upload-service.d.ts +68 -0
- package/dist/file-upload-service.js +110 -0
- package/dist/gemini-schema-utils.d.ts +17 -0
- package/dist/gemini-schema-utils.js +76 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.js +33 -0
- package/dist/llm-client.d.ts +43 -0
- package/dist/llm-client.js +217 -0
- package/dist/media-client.d.ts +42 -0
- package/dist/media-client.js +174 -0
- package/dist/media-transport.d.ts +176 -0
- package/dist/media-transport.js +16 -0
- package/dist/media.d.ts +2 -0
- package/dist/media.js +1 -0
- package/dist/model-detection.d.ts +22 -0
- package/dist/model-detection.js +28 -0
- package/dist/paths.d.ts +2 -0
- package/dist/paths.js +11 -0
- package/dist/provider-def.d.ts +220 -0
- package/dist/provider-def.js +9 -0
- package/dist/provider-registry.d.ts +51 -0
- package/dist/provider-registry.js +130 -0
- package/dist/provider-tool-api.d.ts +44 -0
- package/dist/provider-tool-api.js +9 -0
- package/dist/provider-variant-resolver.d.ts +35 -0
- package/dist/provider-variant-resolver.js +174 -0
- package/dist/retry.d.ts +37 -0
- package/dist/retry.js +71 -0
- package/dist/transport.d.ts +281 -0
- package/dist/transport.js +27 -0
- package/dist/transports/anthropic-messages.d.ts +65 -0
- package/dist/transports/anthropic-messages.js +1004 -0
- package/dist/transports/gemini-cache-api.d.ts +86 -0
- package/dist/transports/gemini-cache-api.js +141 -0
- package/dist/transports/gemini-file-api.d.ts +90 -0
- package/dist/transports/gemini-file-api.js +164 -0
- package/dist/transports/gemini-generatecontent.d.ts +56 -0
- package/dist/transports/gemini-generatecontent.js +688 -0
- package/dist/transports/gemini-lyria-realtime.d.ts +117 -0
- package/dist/transports/gemini-lyria-realtime.js +295 -0
- package/dist/transports/gemini-media.d.ts +53 -0
- package/dist/transports/gemini-media.js +383 -0
- package/dist/transports/media-resolve.d.ts +50 -0
- package/dist/transports/media-resolve.js +91 -0
- package/dist/transports/minimax-media.d.ts +56 -0
- package/dist/transports/minimax-media.js +433 -0
- package/dist/transports/openai-chat.d.ts +81 -0
- package/dist/transports/openai-chat.js +782 -0
- package/dist/transports/openai-media.d.ts +24 -0
- package/dist/transports/openai-media.js +118 -0
- package/dist/transports/openai-responses.d.ts +63 -0
- package/dist/transports/openai-responses.js +778 -0
- package/dist/transports/qwen-media.d.ts +59 -0
- package/dist/transports/qwen-media.js +411 -0
- package/dist/transports/realtime-transport.d.ts +183 -0
- package/dist/transports/realtime-transport.js +332 -0
- package/dist/transports/volcengine-grounding.d.ts +58 -0
- package/dist/transports/volcengine-grounding.js +69 -0
- package/dist/transports/volcengine-media.d.ts +94 -0
- package/dist/transports/volcengine-media.js +801 -0
- package/dist/transports/volcengine-responses.d.ts +64 -0
- package/dist/transports/volcengine-responses.js +797 -0
- package/dist/transports/zhipu-media.d.ts +82 -0
- package/dist/transports/zhipu-media.js +522 -0
- package/dist/transports/zhipu-tool-api.d.ts +35 -0
- package/dist/transports/zhipu-tool-api.js +126 -0
- package/dist/wire-types.d.ts +51 -0
- package/dist/wire-types.js +1 -0
- package/package.json +33 -0
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MiniMax Media Transport 鈥?Music + Video + TTS Generation.
|
|
3
|
+
*
|
|
4
|
+
* Music: POST /v1/music_generation (sync or async poll)
|
|
5
|
+
* Video: POST /v1/video_generation (4 modes: text, image, first-last-frame, subject-ref)
|
|
6
|
+
* Video Query: GET /v1/query/video_generation?task_id=XXX
|
|
7
|
+
* File Retrieve: GET /v1/files/retrieve?file_id=XXX (get download_url)
|
|
8
|
+
*
|
|
9
|
+
* Auth: Authorization: Bearer $MINIMAX_API_KEY
|
|
10
|
+
* Docs: minimax-ProviderMax.md 搂13-18 (video), 搂21 (music), 搂24-28 (files)
|
|
11
|
+
*/
|
|
12
|
+
const DEFAULT_TIMEOUT_MS = 60_000;
|
|
13
|
+
const POLL_INTERVAL_MS = 3_000;
|
|
14
|
+
const MAX_POLL_MS = 600_000; // 10 min
|
|
15
|
+
export class MiniMaxMediaTransport {
|
|
16
|
+
supportedTypes = ["music", "video", "tts"];
|
|
17
|
+
baseUrl;
|
|
18
|
+
timeoutMs;
|
|
19
|
+
constructor(config) {
|
|
20
|
+
// MiniMax music API lives at api.minimaxi.com, not the /anthropic subpath
|
|
21
|
+
this.baseUrl = config.baseUrl
|
|
22
|
+
.replace(/\/anthropic\/?$/, "")
|
|
23
|
+
.replace(/\/+$/, "");
|
|
24
|
+
this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
25
|
+
}
|
|
26
|
+
async generate(request, apiKey, signal) {
|
|
27
|
+
switch (request.mediaType) {
|
|
28
|
+
case "music":
|
|
29
|
+
return this.generateMusic(request, apiKey, signal);
|
|
30
|
+
case "video":
|
|
31
|
+
return this.generateVideo(request, apiKey, signal);
|
|
32
|
+
case "tts":
|
|
33
|
+
return this.generateTTS(request, apiKey, signal);
|
|
34
|
+
default:
|
|
35
|
+
throw new Error(`MiniMaxMediaTransport: unsupported mediaType "${request.mediaType}"`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
async generateTTS(request, apiKey, signal) {
|
|
39
|
+
const start = Date.now();
|
|
40
|
+
const text = request.text ?? request.prompt;
|
|
41
|
+
if (!text) {
|
|
42
|
+
throw new Error("MiniMaxMediaTransport: text or prompt is required for TTS");
|
|
43
|
+
}
|
|
44
|
+
const format = request.audioFormat ?? "mp3";
|
|
45
|
+
const url = `${this.baseUrl}/v1/t2a_v2`;
|
|
46
|
+
const body = {
|
|
47
|
+
model: request.model || "speech-2.8-turbo",
|
|
48
|
+
text,
|
|
49
|
+
stream: false,
|
|
50
|
+
output_format: "url",
|
|
51
|
+
voice_setting: {
|
|
52
|
+
voice_id: request.voice ?? "male-qn-qingse",
|
|
53
|
+
speed: request.speed ?? 1,
|
|
54
|
+
vol: request.metadata?.volume ?? 1,
|
|
55
|
+
pitch: request.metadata?.pitch ?? 0,
|
|
56
|
+
},
|
|
57
|
+
audio_setting: {
|
|
58
|
+
sample_rate: request.metadata?.sampleRate ?? 32000,
|
|
59
|
+
bitrate: request.metadata?.bitrate ?? 128000,
|
|
60
|
+
format,
|
|
61
|
+
channel: request.metadata?.channel ?? 1,
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
const res = await fetch(url, {
|
|
65
|
+
method: "POST",
|
|
66
|
+
headers: {
|
|
67
|
+
"Content-Type": "application/json",
|
|
68
|
+
Authorization: `Bearer ${apiKey}`,
|
|
69
|
+
},
|
|
70
|
+
body: JSON.stringify(body),
|
|
71
|
+
signal: signal ?? AbortSignal.timeout(this.timeoutMs),
|
|
72
|
+
});
|
|
73
|
+
if (!res.ok) {
|
|
74
|
+
const textBody = await res.text().catch(() => "");
|
|
75
|
+
throw new Error(`MiniMax TTS error ${res.status}: ${textBody}`);
|
|
76
|
+
}
|
|
77
|
+
const data = await res.json();
|
|
78
|
+
if (data.base_resp?.status_code && data.base_resp.status_code !== 0) {
|
|
79
|
+
throw new Error(`MiniMax TTS rejected: ${data.base_resp.status_msg ?? "unknown"}`);
|
|
80
|
+
}
|
|
81
|
+
const audioUrl = data.data?.audio;
|
|
82
|
+
if (!audioUrl) {
|
|
83
|
+
throw new Error("MiniMax TTS: no audio in response");
|
|
84
|
+
}
|
|
85
|
+
const characters = data.extra_info?.usage_characters ?? text.length;
|
|
86
|
+
return {
|
|
87
|
+
mediaUrls: [audioUrl],
|
|
88
|
+
model: request.model,
|
|
89
|
+
durationMs: Date.now() - start,
|
|
90
|
+
billingUnit: "per_character",
|
|
91
|
+
billingQuantity: characters,
|
|
92
|
+
metadata: {
|
|
93
|
+
traceId: data.trace_id,
|
|
94
|
+
status: data.data?.status,
|
|
95
|
+
extraInfo: data.extra_info,
|
|
96
|
+
billableUnits: {
|
|
97
|
+
characters,
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
// 鈹€鈹€ Music Generation 鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€
|
|
103
|
+
async generateMusic(request, apiKey, signal) {
|
|
104
|
+
const start = Date.now();
|
|
105
|
+
// MiniMax music API is synchronous 鈥?single POST returns audio directly.
|
|
106
|
+
// Music generation can take 60-300s, so use MAX_POLL_MS as timeout.
|
|
107
|
+
const url = `${this.baseUrl}/v1/music_generation`;
|
|
108
|
+
const body = {
|
|
109
|
+
model: request.model,
|
|
110
|
+
prompt: request.prompt,
|
|
111
|
+
output_format: "url",
|
|
112
|
+
};
|
|
113
|
+
if (request.lyrics)
|
|
114
|
+
body.lyrics = request.lyrics;
|
|
115
|
+
if (request.duration)
|
|
116
|
+
body.duration = request.duration;
|
|
117
|
+
// MiniMax music-2.6 requires either lyrics or is_instrumental:true
|
|
118
|
+
if (request.isInstrumental || !request.lyrics)
|
|
119
|
+
body.is_instrumental = true;
|
|
120
|
+
if (request.audioUrl)
|
|
121
|
+
body.audio_url = request.audioUrl;
|
|
122
|
+
if (request.lyricsOptimizer)
|
|
123
|
+
body.lyrics_optimizer = true;
|
|
124
|
+
if (request.audioFormat || request.voice) {
|
|
125
|
+
body.audio_setting = {
|
|
126
|
+
sample_rate: 44100,
|
|
127
|
+
bitrate: 256000,
|
|
128
|
+
format: request.audioFormat ?? "mp3",
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
const res = await fetch(url, {
|
|
132
|
+
method: "POST",
|
|
133
|
+
headers: {
|
|
134
|
+
"Content-Type": "application/json",
|
|
135
|
+
Authorization: `Bearer ${apiKey}`,
|
|
136
|
+
},
|
|
137
|
+
body: JSON.stringify(body),
|
|
138
|
+
signal: signal ?? AbortSignal.timeout(MAX_POLL_MS),
|
|
139
|
+
});
|
|
140
|
+
if (!res.ok) {
|
|
141
|
+
const text = await res.text().catch(() => "");
|
|
142
|
+
throw new Error(`MiniMax music error ${res.status}: ${text}`);
|
|
143
|
+
}
|
|
144
|
+
const data = await res.json();
|
|
145
|
+
if (data.base_resp?.status_code && data.base_resp.status_code !== 0) {
|
|
146
|
+
throw new Error(`MiniMax music rejected: ${data.base_resp.status_msg ?? "unknown"}`);
|
|
147
|
+
}
|
|
148
|
+
// Sync response: data.audio (primary) or audio_file (legacy)
|
|
149
|
+
const audioUrl = data.data?.audio ?? data.audio_file;
|
|
150
|
+
if (audioUrl) {
|
|
151
|
+
return {
|
|
152
|
+
mediaUrls: [audioUrl],
|
|
153
|
+
model: request.model,
|
|
154
|
+
durationMs: Date.now() - start,
|
|
155
|
+
billingUnit: "per_second",
|
|
156
|
+
billingQuantity: request.duration ?? 0,
|
|
157
|
+
metadata: {
|
|
158
|
+
billableUnits: {
|
|
159
|
+
seconds: request.duration ?? 0,
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
// Async fallback: poll by task_id
|
|
165
|
+
const taskId = data.task_id;
|
|
166
|
+
if (!taskId) {
|
|
167
|
+
throw new Error("MiniMax music: no audio_file or task_id in response");
|
|
168
|
+
}
|
|
169
|
+
const pollUrl = `${this.baseUrl}/v1/music_generation/${taskId}`;
|
|
170
|
+
const result = await this.pollTask(pollUrl, apiKey, signal, request.onProgress, taskId);
|
|
171
|
+
return {
|
|
172
|
+
mediaUrls: result.audioUrls,
|
|
173
|
+
model: request.model,
|
|
174
|
+
durationMs: Date.now() - start,
|
|
175
|
+
billingUnit: "per_second",
|
|
176
|
+
billingQuantity: request.duration ?? 0,
|
|
177
|
+
metadata: {
|
|
178
|
+
billableUnits: {
|
|
179
|
+
seconds: request.duration ?? 0,
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
async pollTask(url, apiKey, signal, onProgress, taskId) {
|
|
185
|
+
const deadline = Date.now() + MAX_POLL_MS;
|
|
186
|
+
while (Date.now() < deadline) {
|
|
187
|
+
signal?.throwIfAborted();
|
|
188
|
+
const res = await fetch(url, {
|
|
189
|
+
method: "GET",
|
|
190
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
191
|
+
signal: signal ?? AbortSignal.timeout(this.timeoutMs),
|
|
192
|
+
});
|
|
193
|
+
if (!res.ok) {
|
|
194
|
+
const text = await res.text().catch(() => "");
|
|
195
|
+
throw new Error(`MiniMax music poll error ${res.status}: ${text}`);
|
|
196
|
+
}
|
|
197
|
+
const data = await res.json();
|
|
198
|
+
const status = data.status;
|
|
199
|
+
if (status === "Success" || status === "Finished") {
|
|
200
|
+
onProgress?.(100, "completed", taskId);
|
|
201
|
+
const audioUrls = [];
|
|
202
|
+
if (data.audio_file)
|
|
203
|
+
audioUrls.push(data.audio_file);
|
|
204
|
+
return { audioUrls };
|
|
205
|
+
}
|
|
206
|
+
if (status === "Failed" || status === "Cancelled") {
|
|
207
|
+
throw new Error(`MiniMax music task failed: ${data.base_resp?.status_msg ?? "unknown"}`);
|
|
208
|
+
}
|
|
209
|
+
// Report progress estimate
|
|
210
|
+
const elapsed = Date.now() - (deadline - MAX_POLL_MS);
|
|
211
|
+
onProgress?.(Math.min(95, Math.round((elapsed / MAX_POLL_MS) * 100)), status ?? "running", taskId);
|
|
212
|
+
await new Promise(r => setTimeout(r, POLL_INTERVAL_MS));
|
|
213
|
+
}
|
|
214
|
+
throw new Error("MiniMax music task timed out after polling");
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Generate lyrics from a text prompt via MiniMax Lyrics Generation API.
|
|
218
|
+
* POST /v1/lyrics_generation 鈥?returns structured lyrics with tags.
|
|
219
|
+
*/
|
|
220
|
+
async generateLyrics(prompt, apiKey, signal) {
|
|
221
|
+
const url = `${this.baseUrl}/v1/lyrics_generation`;
|
|
222
|
+
const res = await fetch(url, {
|
|
223
|
+
method: "POST",
|
|
224
|
+
headers: {
|
|
225
|
+
"Content-Type": "application/json",
|
|
226
|
+
Authorization: `Bearer ${apiKey}`,
|
|
227
|
+
},
|
|
228
|
+
body: JSON.stringify({ prompt }),
|
|
229
|
+
signal: signal ?? AbortSignal.timeout(this.timeoutMs),
|
|
230
|
+
});
|
|
231
|
+
if (!res.ok) {
|
|
232
|
+
const text = await res.text().catch(() => "");
|
|
233
|
+
throw new Error(`MiniMax lyrics API error ${res.status}: ${text}`);
|
|
234
|
+
}
|
|
235
|
+
const data = await res.json();
|
|
236
|
+
if (data.base_resp?.status_code && data.base_resp.status_code !== 0) {
|
|
237
|
+
throw new Error(`MiniMax lyrics error: ${data.base_resp.status_msg ?? "unknown"}`);
|
|
238
|
+
}
|
|
239
|
+
return data.data?.lyrics ?? "";
|
|
240
|
+
}
|
|
241
|
+
// 鈹€鈹€ Video Generation (minimax-ProviderMax 搂13-16) 鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€
|
|
242
|
+
// 4 modes: text-to-video, image-to-video, first-last-frame, subject-reference
|
|
243
|
+
// All submit to POST /v1/video_generation 鈫?async task 鈫?poll 鈫?file_id 鈫?download
|
|
244
|
+
async generateVideo(request, apiKey, signal) {
|
|
245
|
+
const start = Date.now();
|
|
246
|
+
const url = `${this.baseUrl}/v1/video_generation`;
|
|
247
|
+
const body = {
|
|
248
|
+
model: request.model || "S2V-01",
|
|
249
|
+
prompt: request.prompt,
|
|
250
|
+
};
|
|
251
|
+
// Route to correct video mode based on available inputs
|
|
252
|
+
if (request.referenceImages?.length && request.imageRoles) {
|
|
253
|
+
// Subject reference mode (搂16) or first-last-frame mode (搂15)
|
|
254
|
+
const hasFirstFrame = request.imageRoles.includes("first_frame");
|
|
255
|
+
const hasLastFrame = request.imageRoles.includes("last_frame");
|
|
256
|
+
if (hasFirstFrame && hasLastFrame) {
|
|
257
|
+
// First-last-frame mode (搂15)
|
|
258
|
+
const firstIdx = request.imageRoles.indexOf("first_frame");
|
|
259
|
+
const lastIdx = request.imageRoles.indexOf("last_frame");
|
|
260
|
+
body.first_frame_image = request.referenceImages[firstIdx];
|
|
261
|
+
body.last_frame_image = request.referenceImages[lastIdx];
|
|
262
|
+
}
|
|
263
|
+
else if (hasFirstFrame) {
|
|
264
|
+
// Image-to-video mode (搂14): single image as first frame
|
|
265
|
+
const idx = request.imageRoles.indexOf("first_frame");
|
|
266
|
+
body.first_frame_image = request.referenceImages[idx];
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
// Subject reference mode (搂16)
|
|
270
|
+
body.subject_reference = request.referenceImages.map((imgUrl, i) => ({
|
|
271
|
+
type: "character",
|
|
272
|
+
image: imgUrl,
|
|
273
|
+
}));
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
else if (request.imageUrl) {
|
|
277
|
+
// Image-to-video mode (搂14): imageUrl as first frame
|
|
278
|
+
body.first_frame_image = request.imageUrl;
|
|
279
|
+
}
|
|
280
|
+
// Otherwise: text-to-video mode (搂13), just prompt
|
|
281
|
+
if (request.callbackUrl)
|
|
282
|
+
body.callback_url = request.callbackUrl;
|
|
283
|
+
const res = await fetch(url, {
|
|
284
|
+
method: "POST",
|
|
285
|
+
headers: {
|
|
286
|
+
"Content-Type": "application/json",
|
|
287
|
+
Authorization: `Bearer ${apiKey}`,
|
|
288
|
+
},
|
|
289
|
+
body: JSON.stringify(body),
|
|
290
|
+
signal: signal ?? AbortSignal.timeout(this.timeoutMs),
|
|
291
|
+
});
|
|
292
|
+
if (!res.ok) {
|
|
293
|
+
const text = await res.text().catch(() => "");
|
|
294
|
+
throw new Error(`MiniMax video submit error ${res.status}: ${text}`);
|
|
295
|
+
}
|
|
296
|
+
const data = await res.json();
|
|
297
|
+
if (data.base_resp?.status_code && data.base_resp.status_code !== 0) {
|
|
298
|
+
throw new Error(`MiniMax video rejected: ${data.base_resp.status_msg ?? "unknown"}`);
|
|
299
|
+
}
|
|
300
|
+
const taskId = data.task_id;
|
|
301
|
+
if (!taskId) {
|
|
302
|
+
throw new Error("MiniMax video: no task_id in response");
|
|
303
|
+
}
|
|
304
|
+
// Poll for completion
|
|
305
|
+
const result = await this.pollVideoTask(taskId, apiKey, signal, request.onProgress);
|
|
306
|
+
return {
|
|
307
|
+
mediaUrls: result.downloadUrl ? [result.downloadUrl] : [],
|
|
308
|
+
model: request.model || "S2V-01",
|
|
309
|
+
durationMs: Date.now() - start,
|
|
310
|
+
taskId,
|
|
311
|
+
billingUnit: "per_second",
|
|
312
|
+
billingQuantity: request.duration ?? 0,
|
|
313
|
+
metadata: {
|
|
314
|
+
billableUnits: {
|
|
315
|
+
videos: 1,
|
|
316
|
+
seconds: request.duration ?? 0,
|
|
317
|
+
},
|
|
318
|
+
fileId: result.fileId,
|
|
319
|
+
width: result.width,
|
|
320
|
+
height: result.height,
|
|
321
|
+
},
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
async pollVideoTask(taskId, apiKey, signal, onProgress) {
|
|
325
|
+
const deadline = Date.now() + MAX_POLL_MS;
|
|
326
|
+
while (Date.now() < deadline) {
|
|
327
|
+
signal?.throwIfAborted();
|
|
328
|
+
const queryUrl = `${this.baseUrl}/v1/query/video_generation?task_id=${encodeURIComponent(taskId)}`;
|
|
329
|
+
const res = await fetch(queryUrl, {
|
|
330
|
+
method: "GET",
|
|
331
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
332
|
+
signal: signal ?? AbortSignal.timeout(this.timeoutMs),
|
|
333
|
+
});
|
|
334
|
+
if (!res.ok) {
|
|
335
|
+
const text = await res.text().catch(() => "");
|
|
336
|
+
throw new Error(`MiniMax video poll error ${res.status}: ${text}`);
|
|
337
|
+
}
|
|
338
|
+
const data = await res.json();
|
|
339
|
+
if (data.base_resp?.status_code && data.base_resp.status_code !== 0) {
|
|
340
|
+
// 1026=input sensitive content, 1027=output sensitive content
|
|
341
|
+
throw new Error(`MiniMax video task error ${data.base_resp.status_code}: ${data.base_resp.status_msg ?? ""}`);
|
|
342
|
+
}
|
|
343
|
+
const status = data.status;
|
|
344
|
+
if (status === "Success") {
|
|
345
|
+
onProgress?.(100, "completed", taskId);
|
|
346
|
+
// Get download URL via file retrieve endpoint
|
|
347
|
+
let downloadUrl;
|
|
348
|
+
if (data.file_id) {
|
|
349
|
+
try {
|
|
350
|
+
downloadUrl = await this.getFileDownloadUrl(data.file_id, apiKey, signal);
|
|
351
|
+
}
|
|
352
|
+
catch {
|
|
353
|
+
// Non-fatal: file_id is still usable
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return {
|
|
357
|
+
fileId: data.file_id,
|
|
358
|
+
downloadUrl,
|
|
359
|
+
width: data.video_width,
|
|
360
|
+
height: data.video_height,
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
if (status === "Fail") {
|
|
364
|
+
throw new Error(`MiniMax video task failed: ${data.base_resp?.status_msg ?? "unknown"}`);
|
|
365
|
+
}
|
|
366
|
+
// Preparing / Queueing / Processing 鈥?report progress
|
|
367
|
+
const statusLabel = status ?? "running";
|
|
368
|
+
const elapsed = Date.now() - (deadline - MAX_POLL_MS);
|
|
369
|
+
const estimatedPercent = Math.min(95, Math.round((elapsed / MAX_POLL_MS) * 100));
|
|
370
|
+
onProgress?.(estimatedPercent, statusLabel, taskId);
|
|
371
|
+
await new Promise(r => setTimeout(r, POLL_INTERVAL_MS));
|
|
372
|
+
}
|
|
373
|
+
throw new Error("MiniMax video task timed out after polling");
|
|
374
|
+
}
|
|
375
|
+
// 鈹€鈹€ File Retrieve (minimax-ProviderMax 搂26) 鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€
|
|
376
|
+
// GET /v1/files/retrieve?file_id=XXX 鈫?{ download_url, bytes, filename, ... }
|
|
377
|
+
async getFileDownloadUrl(fileId, apiKey, signal) {
|
|
378
|
+
const url = `${this.baseUrl}/v1/files/retrieve?file_id=${encodeURIComponent(fileId)}`;
|
|
379
|
+
const res = await fetch(url, {
|
|
380
|
+
method: "GET",
|
|
381
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
382
|
+
signal: signal ?? AbortSignal.timeout(this.timeoutMs),
|
|
383
|
+
});
|
|
384
|
+
if (!res.ok) {
|
|
385
|
+
const text = await res.text().catch(() => "");
|
|
386
|
+
throw new Error(`MiniMax file retrieve error ${res.status}: ${text}`);
|
|
387
|
+
}
|
|
388
|
+
const data = await res.json();
|
|
389
|
+
const downloadUrl = data.file?.download_url;
|
|
390
|
+
if (!downloadUrl) {
|
|
391
|
+
throw new Error("MiniMax file retrieve: no download_url in response");
|
|
392
|
+
}
|
|
393
|
+
return downloadUrl;
|
|
394
|
+
}
|
|
395
|
+
// 鈹€鈹€ AsyncMediaTransport: Task Management 鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€
|
|
396
|
+
/**
|
|
397
|
+
* Query a single video task by ID.
|
|
398
|
+
* GET /v1/query/video_generation?task_id=XXX
|
|
399
|
+
*/
|
|
400
|
+
async getTaskStatus(taskId, apiKey, signal) {
|
|
401
|
+
const url = `${this.baseUrl}/v1/query/video_generation?task_id=${encodeURIComponent(taskId)}`;
|
|
402
|
+
const res = await fetch(url, {
|
|
403
|
+
method: "GET",
|
|
404
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
405
|
+
signal: signal ?? AbortSignal.timeout(this.timeoutMs),
|
|
406
|
+
});
|
|
407
|
+
if (!res.ok) {
|
|
408
|
+
const text = await res.text().catch(() => "");
|
|
409
|
+
throw new Error(`MiniMax task status error ${res.status}: ${text}`);
|
|
410
|
+
}
|
|
411
|
+
const data = await res.json();
|
|
412
|
+
const rawStatus = data.status ?? "unknown";
|
|
413
|
+
// Normalize MiniMax statuses to lower-case
|
|
414
|
+
const normalizedStatus = rawStatus === "Success" ? "succeeded"
|
|
415
|
+
: rawStatus === "Fail" ? "failed"
|
|
416
|
+
: rawStatus.toLowerCase();
|
|
417
|
+
return { status: normalizedStatus, task: data };
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* List tasks 鈥?MiniMax does not have a bulk list endpoint.
|
|
421
|
+
* Each task must be queried individually with getTaskStatus().
|
|
422
|
+
*/
|
|
423
|
+
async listVideoTasks(_apiKey, _options, _signal) {
|
|
424
|
+
// MiniMax has no list endpoint; callers should use getTaskStatus() for direct lookup
|
|
425
|
+
return { data: [] };
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Cancel/delete is not natively supported by MiniMax video API.
|
|
429
|
+
*/
|
|
430
|
+
async deleteVideoTask(_taskId, _apiKey, _signal) {
|
|
431
|
+
throw new Error("MiniMax does not support video task cancellation.");
|
|
432
|
+
}
|
|
433
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAI Chat Completions TransportSSE streaming implementation.
|
|
3
|
+
*
|
|
4
|
+
* Covers all OpenAI-compatible providers:
|
|
5
|
+
* DeepSeek, Qwen, Minimax, Moonshot, OpenRouter, etc.
|
|
6
|
+
*
|
|
7
|
+
* POST {baseUrl}/v1/chat/completions with stream: true
|
|
8
|
+
* Auth: Authorization: Bearer {apiKey}
|
|
9
|
+
*
|
|
10
|
+
* SSE format: lines prefixed with "data: ", JSON parsing per event.
|
|
11
|
+
*
|
|
12
|
+
* Adapted from admin-infer-proxy-client.ts SSE logic + Hermes openai_chat.py transport.
|
|
13
|
+
*/
|
|
14
|
+
import type { LLMChunk, LLMRequest, LLMTransport, FIMRequest, FIMChunk } from "../transport.js";
|
|
15
|
+
import type { ProviderQuirks } from "../provider-def.js";
|
|
16
|
+
import type { FileUploadAdapter } from "../file-upload-service.js";
|
|
17
|
+
export interface OpenAIChatTransportConfig {
|
|
18
|
+
baseUrl: string;
|
|
19
|
+
/** Additional headers (e.g. for specific providers) */
|
|
20
|
+
extraHeaders?: Record<string, string>;
|
|
21
|
+
/** Timeout in ms (default 180_000) */
|
|
22
|
+
timeoutMs?: number;
|
|
23
|
+
/** Whether to include stream_options (default true). Set false for providers that reject it. */
|
|
24
|
+
supportsStreamOptions?: boolean;
|
|
25
|
+
/** Whether to omit temperature when it equals 0 (e.g. Moonshot rejects 0) */
|
|
26
|
+
omitZeroTemperature?: boolean;
|
|
27
|
+
/** Provider-specific quirks (CC/altcode parity) */
|
|
28
|
+
quirks?: ProviderQuirks;
|
|
29
|
+
/** File upload adapter for resolving local media URLs via upload instead of base64. */
|
|
30
|
+
fileUploadAdapter?: FileUploadAdapter;
|
|
31
|
+
}
|
|
32
|
+
export declare class OpenAIChatTransport implements LLMTransport {
|
|
33
|
+
private baseUrl;
|
|
34
|
+
private extraHeaders;
|
|
35
|
+
private timeoutMs;
|
|
36
|
+
private supportsStreamOptions;
|
|
37
|
+
private omitZeroTemperature;
|
|
38
|
+
private quirks;
|
|
39
|
+
private fileUploadAdapter?;
|
|
40
|
+
private cumulativeReasoningLen;
|
|
41
|
+
private cumulativeContentLen;
|
|
42
|
+
constructor(config: OpenAIChatTransportConfig);
|
|
43
|
+
stream(request: LLMRequest, apiKey: string, signal?: AbortSignal): AsyncGenerator<LLMChunk>;
|
|
44
|
+
private fetchAndStream;
|
|
45
|
+
/**
|
|
46
|
+
* Handle non-streaming JSON response from providers that ignore stream:true.
|
|
47
|
+
* Synthesize the same LLMChunk events a streaming response would produce.
|
|
48
|
+
*/
|
|
49
|
+
private handleNonStreamingResponse;
|
|
50
|
+
/**
|
|
51
|
+
* Parse SSE stream with 90s idle watchdog (CC parity).
|
|
52
|
+
* If no data arrives within STREAM_IDLE_TIMEOUT_MS, throw to trigger retry.
|
|
53
|
+
*/
|
|
54
|
+
private parseSSEStreamWithWatchdog;
|
|
55
|
+
private processChunk;
|
|
56
|
+
/**
|
|
57
|
+
* FIM completion via /beta/v1/completions.
|
|
58
|
+
* Only works with DeepSeek (requires supportsPrefixCompletion quirk).
|
|
59
|
+
* Non-thinking mode only; max completion 4K tokens.
|
|
60
|
+
*/
|
|
61
|
+
complete(request: FIMRequest, apiKey: string, signal?: AbortSignal): AsyncGenerator<FIMChunk>;
|
|
62
|
+
/**
|
|
63
|
+
* Upload a file for use in conversations (Kimi File API).
|
|
64
|
+
* Returns a file_id that can be referenced in user messages.
|
|
65
|
+
* POST /v1/files with multipart/form-data.
|
|
66
|
+
*/
|
|
67
|
+
uploadFile(fileBlob: Blob, filename: string, purpose: string, apiKey: string, signal?: AbortSignal): Promise<{
|
|
68
|
+
fileId: string;
|
|
69
|
+
filename: string;
|
|
70
|
+
bytes: number;
|
|
71
|
+
}>;
|
|
72
|
+
/**
|
|
73
|
+
* Get file content/statusGET /v1/files/{file_id}
|
|
74
|
+
*/
|
|
75
|
+
getFileInfo(fileId: string, apiKey: string, signal?: AbortSignal): Promise<{
|
|
76
|
+
id: string;
|
|
77
|
+
filename: string;
|
|
78
|
+
bytes: number;
|
|
79
|
+
status: string;
|
|
80
|
+
}>;
|
|
81
|
+
}
|