@mixio-pro/kalaasetu-mcp 2.1.0 → 2.1.1-beta
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/package.json +6 -2
- package/src/index.ts +4 -3
- package/src/storage/index.ts +4 -3
- package/src/tools/fal/config.ts +9 -8
- package/src/tools/fal/dynamic-tools.ts +214 -237
- package/src/tools/fal/models.ts +115 -93
- package/src/tools/fal/storage.ts +66 -61
- package/src/tools/gemini.ts +302 -281
- package/src/tools/get-status.ts +50 -46
- package/src/tools/image-to-video.ts +309 -300
- package/src/tools/perplexity.ts +188 -172
- package/src/tools/youtube.ts +45 -41
- package/src/utils/llm-prompt-enhancer.ts +3 -2
- package/src/utils/logger.ts +71 -0
- package/src/utils/openmeter.ts +123 -0
- package/src/utils/prompt-enhancer-presets.ts +7 -5
- package/src/utils/remote-sync.ts +19 -10
- package/src/utils/tool-credits.ts +104 -0
- package/src/utils/tool-wrapper.ts +37 -6
- package/src/utils/url-file.ts +4 -3
- package/src/test-context.ts +0 -52
- package/src/test-error-handling.ts +0 -31
- package/src/tools/image-to-video.sdk-backup.ts +0 -218
|
@@ -179,357 +179,366 @@ export const imageToVideo = {
|
|
|
179
179
|
};
|
|
180
180
|
},
|
|
181
181
|
) {
|
|
182
|
-
return safeToolExecute(
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
Math.abs(
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
if (
|
|
214
|
-
durationSeconds === 6 &&
|
|
215
|
-
parseInt(args.duration_seconds || "6") === 7
|
|
216
|
-
) {
|
|
217
|
-
durationSeconds = 8;
|
|
218
|
-
}
|
|
219
|
-
// Stream diagnostic info about auth
|
|
220
|
-
let token: string;
|
|
221
|
-
try {
|
|
222
|
-
if (context?.streamContent) {
|
|
223
|
-
await context.streamContent({
|
|
224
|
-
type: "text" as const,
|
|
225
|
-
text: `[Vertex] Authenticating with Google Cloud (project: ${projectId}, location: ${location})...`,
|
|
226
|
-
});
|
|
182
|
+
return safeToolExecute(
|
|
183
|
+
async () => {
|
|
184
|
+
const projectId = "mixio-pro";
|
|
185
|
+
const location = "us-central1";
|
|
186
|
+
const modelId = args.model_id || "veo-3.1-fast-generate-preview";
|
|
187
|
+
|
|
188
|
+
// Validate and parse duration_seconds - snap to nearest 4, 6, or 8
|
|
189
|
+
let durationSeconds = parseInt(args.duration_seconds || "6");
|
|
190
|
+
if (isNaN(durationSeconds)) durationSeconds = 6;
|
|
191
|
+
|
|
192
|
+
const validDurations = [4, 6, 8];
|
|
193
|
+
// Find nearest valid duration
|
|
194
|
+
durationSeconds = validDurations.reduce((prev, curr) => {
|
|
195
|
+
return Math.abs(curr - durationSeconds) <
|
|
196
|
+
Math.abs(prev - durationSeconds)
|
|
197
|
+
? curr
|
|
198
|
+
: prev;
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// Tie-breaking: if equidistant (e.g. 5), the reduce above keeps the first one (4) because < is strict.
|
|
202
|
+
// However, user requested "nearest duration with the ceil", effectively meaning round up if equidistant.
|
|
203
|
+
// Let's explicitly handle the equidistant cases or just use a custom finder.
|
|
204
|
+
// 5 -> equidistant to 4 and 6. "With ceil" implies 6.
|
|
205
|
+
// 7 -> equidistant to 6 and 8. "With ceil" implies 8.
|
|
206
|
+
|
|
207
|
+
// Simpler logic for these specific values:
|
|
208
|
+
if (
|
|
209
|
+
durationSeconds === 4 &&
|
|
210
|
+
parseInt(args.duration_seconds || "6") === 5
|
|
211
|
+
) {
|
|
212
|
+
durationSeconds = 6;
|
|
227
213
|
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
});
|
|
214
|
+
if (
|
|
215
|
+
durationSeconds === 6 &&
|
|
216
|
+
parseInt(args.duration_seconds || "6") === 7
|
|
217
|
+
) {
|
|
218
|
+
durationSeconds = 8;
|
|
234
219
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
220
|
+
// Stream diagnostic info about auth
|
|
221
|
+
let token: string;
|
|
222
|
+
try {
|
|
223
|
+
if (context?.streamContent) {
|
|
224
|
+
await context.streamContent({
|
|
225
|
+
type: "text" as const,
|
|
226
|
+
text: `[Vertex] Authenticating with Google Cloud (project: ${projectId}, location: ${location})...`,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
token = await getGoogleAccessToken();
|
|
230
|
+
if (context?.streamContent) {
|
|
231
|
+
await context.streamContent({
|
|
232
|
+
type: "text" as const,
|
|
233
|
+
text: `[Vertex] ✓ Authentication successful. Token acquired.`,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
} catch (authError: any) {
|
|
237
|
+
const errorMsg = authError?.message || String(authError);
|
|
238
|
+
if (context?.streamContent) {
|
|
239
|
+
await context.streamContent({
|
|
240
|
+
type: "text" as const,
|
|
241
|
+
text: `[Vertex] ✗ Authentication FAILED: ${errorMsg}. Check GOOGLE_APPLICATION_CREDENTIALS or run 'gcloud auth application-default login'.`,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
throw new Error(`Google Cloud authentication failed: ${errorMsg}`);
|
|
257
245
|
}
|
|
258
|
-
}
|
|
259
|
-
let current: any;
|
|
260
246
|
|
|
261
|
-
|
|
262
|
-
if (!args.prompt) {
|
|
263
|
-
throw new Error("prompt is required when starting a new generation.");
|
|
264
|
-
}
|
|
247
|
+
const fetchUrl = `https://${location}-aiplatform.googleapis.com/v1/projects/${projectId}/locations/${location}/publishers/google/models/${modelId}:fetchPredictOperation`;
|
|
265
248
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
249
|
+
// If resuming, reconstruct the full operation path from the UUID
|
|
250
|
+
let operationName: string | undefined;
|
|
251
|
+
if (args.resume_endpoint) {
|
|
252
|
+
// Support both UUID-only and full path formats
|
|
253
|
+
if (args.resume_endpoint.includes("/")) {
|
|
254
|
+
operationName = args.resume_endpoint; // Already a full path
|
|
255
|
+
} else {
|
|
256
|
+
// Reconstruct full path from UUID
|
|
257
|
+
operationName = `projects/${projectId}/locations/${location}/publishers/google/models/${modelId}/operations/${args.resume_endpoint}`;
|
|
258
|
+
}
|
|
271
259
|
}
|
|
260
|
+
let current: any;
|
|
272
261
|
|
|
273
|
-
|
|
262
|
+
if (!operationName) {
|
|
263
|
+
if (!args.prompt) {
|
|
264
|
+
throw new Error(
|
|
265
|
+
"prompt is required when starting a new generation.",
|
|
266
|
+
);
|
|
267
|
+
}
|
|
274
268
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
mimeType,
|
|
282
|
-
},
|
|
283
|
-
};
|
|
284
|
-
}
|
|
269
|
+
if (context?.streamContent) {
|
|
270
|
+
await context.streamContent({
|
|
271
|
+
type: "text" as const,
|
|
272
|
+
text: `[Vertex] Submitting video generation request to Veo model: ${modelId}...`,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
285
275
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
276
|
+
const url = `https://${location}-aiplatform.googleapis.com/v1/projects/${projectId}/locations/${location}/publishers/google/models/${modelId}:predictLongRunning`;
|
|
277
|
+
|
|
278
|
+
let imagePart: any = undefined;
|
|
279
|
+
if (args.image_path) {
|
|
280
|
+
const { data, mimeType } = await fileToBase64(args.image_path);
|
|
281
|
+
imagePart = {
|
|
282
|
+
image: {
|
|
283
|
+
bytesBase64Encoded: data,
|
|
284
|
+
mimeType,
|
|
285
|
+
},
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
let lastFramePart: any = undefined;
|
|
290
|
+
if (args.last_frame_path) {
|
|
291
|
+
const { data, mimeType } = await fileToBase64(args.last_frame_path);
|
|
292
|
+
lastFramePart = {
|
|
293
|
+
lastFrame: {
|
|
294
|
+
bytesBase64Encoded: data,
|
|
295
|
+
mimeType,
|
|
296
|
+
},
|
|
297
|
+
};
|
|
298
|
+
}
|
|
296
299
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
300
|
+
let referenceImages: any[] | undefined = undefined;
|
|
301
|
+
if (args.reference_images) {
|
|
302
|
+
let refImages: string[];
|
|
303
|
+
if (typeof args.reference_images === "string") {
|
|
304
|
+
if (
|
|
305
|
+
args.reference_images.startsWith("[") &&
|
|
306
|
+
args.reference_images.endsWith("]")
|
|
307
|
+
) {
|
|
308
|
+
try {
|
|
309
|
+
refImages = JSON.parse(args.reference_images);
|
|
310
|
+
} catch {
|
|
311
|
+
throw new Error("Invalid reference_images format");
|
|
312
|
+
}
|
|
313
|
+
} else {
|
|
314
|
+
refImages = [args.reference_images];
|
|
309
315
|
}
|
|
316
|
+
} else if (Array.isArray(args.reference_images)) {
|
|
317
|
+
refImages = args.reference_images;
|
|
310
318
|
} else {
|
|
311
|
-
|
|
319
|
+
throw new Error(
|
|
320
|
+
"Invalid reference_images: must be array or string",
|
|
321
|
+
);
|
|
312
322
|
}
|
|
313
|
-
} else if (Array.isArray(args.reference_images)) {
|
|
314
|
-
refImages = args.reference_images;
|
|
315
|
-
} else {
|
|
316
|
-
throw new Error(
|
|
317
|
-
"Invalid reference_images: must be array or string",
|
|
318
|
-
);
|
|
319
|
-
}
|
|
320
323
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
324
|
+
if (refImages.length > 0) {
|
|
325
|
+
referenceImages = await Promise.all(
|
|
326
|
+
refImages.slice(0, 3).map(async (p) => {
|
|
327
|
+
const { data, mimeType } = await fileToBase64(p);
|
|
328
|
+
return {
|
|
329
|
+
image: {
|
|
330
|
+
bytesBase64Encoded: data,
|
|
331
|
+
mimeType,
|
|
332
|
+
},
|
|
333
|
+
referenceType: "asset",
|
|
334
|
+
};
|
|
335
|
+
}),
|
|
336
|
+
);
|
|
337
|
+
}
|
|
334
338
|
}
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
const personGeneration =
|
|
338
|
-
args.person_generation ||
|
|
339
|
-
(args.image_path ? "allow_adult" : "allow_all");
|
|
340
339
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
340
|
+
const personGeneration =
|
|
341
|
+
args.person_generation ||
|
|
342
|
+
(args.image_path ? "allow_adult" : "allow_all");
|
|
344
343
|
|
|
345
|
-
|
|
346
|
-
|
|
344
|
+
// Apply prompt enhancement logic
|
|
345
|
+
let enhancedPrompt = args.prompt;
|
|
346
|
+
let enhancedNegativePrompt = args.negative_prompt;
|
|
347
347
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
presetToUse = "veo";
|
|
351
|
-
}
|
|
348
|
+
// Determine which preset to use
|
|
349
|
+
let presetToUse = args.enhancer_preset;
|
|
352
350
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
351
|
+
// If auto_enhance is true and no preset specified, default to 'veo'
|
|
352
|
+
if (args.auto_enhance === true && !presetToUse) {
|
|
353
|
+
presetToUse = "veo";
|
|
354
|
+
}
|
|
357
355
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
await import("../utils/llm-prompt-enhancer");
|
|
363
|
-
|
|
364
|
-
if (isLLMEnhancerAvailable()) {
|
|
365
|
-
if (context?.streamContent) {
|
|
366
|
-
await context.streamContent({
|
|
367
|
-
type: "text" as const,
|
|
368
|
-
text: `[VEO] Enhancing prompt with Gemini for optimal Veo 3.1 generation...`,
|
|
369
|
-
});
|
|
370
|
-
}
|
|
356
|
+
// Disable enhancement if auto_enhance is explicitly false
|
|
357
|
+
if (args.auto_enhance === false) {
|
|
358
|
+
presetToUse = undefined;
|
|
359
|
+
}
|
|
371
360
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
);
|
|
361
|
+
if (presetToUse && args.prompt) {
|
|
362
|
+
// Use LLM-based enhancement for 'veo' preset
|
|
363
|
+
if (presetToUse === "veo") {
|
|
364
|
+
const { enhancePromptWithLLM, isLLMEnhancerAvailable } =
|
|
365
|
+
await import("../utils/llm-prompt-enhancer");
|
|
377
366
|
|
|
367
|
+
if (isLLMEnhancerAvailable()) {
|
|
378
368
|
if (context?.streamContent) {
|
|
379
369
|
await context.streamContent({
|
|
380
370
|
type: "text" as const,
|
|
381
|
-
text: `[VEO]
|
|
371
|
+
text: `[VEO] Enhancing prompt with Gemini for optimal Veo 3.1 generation...`,
|
|
382
372
|
});
|
|
383
373
|
}
|
|
384
|
-
|
|
374
|
+
|
|
375
|
+
try {
|
|
376
|
+
enhancedPrompt = await enhancePromptWithLLM(
|
|
377
|
+
args.prompt,
|
|
378
|
+
"veo",
|
|
379
|
+
);
|
|
380
|
+
context?.log?.info(
|
|
381
|
+
`LLM-enhanced prompt for Veo: "${args.prompt}" → "${enhancedPrompt}"`,
|
|
382
|
+
);
|
|
383
|
+
|
|
384
|
+
if (context?.streamContent) {
|
|
385
|
+
await context.streamContent({
|
|
386
|
+
type: "text" as const,
|
|
387
|
+
text: `[VEO] ✓ Prompt enhanced. Length: ${args.prompt.length} → ${enhancedPrompt.length} chars`,
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
} catch (err: any) {
|
|
391
|
+
context?.log?.info(
|
|
392
|
+
`LLM enhancement failed, using original: ${err.message}`,
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
} else {
|
|
385
396
|
context?.log?.info(
|
|
386
|
-
|
|
397
|
+
"GEMINI_API_KEY not set, skipping Veo LLM enhancement",
|
|
387
398
|
);
|
|
388
399
|
}
|
|
389
400
|
} else {
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
)
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
// Apply negative elements if not already set
|
|
400
|
-
const negatives = enhancer.getNegativeElements();
|
|
401
|
-
if (negatives && !enhancedNegativePrompt) {
|
|
402
|
-
enhancedNegativePrompt = negatives;
|
|
401
|
+
// Fall back to static string-based enhancement for other presets
|
|
402
|
+
const enhancer = resolveEnhancer(presetToUse);
|
|
403
|
+
if (enhancer.hasTransformations()) {
|
|
404
|
+
enhancedPrompt = enhancer.enhance(args.prompt);
|
|
405
|
+
// Apply negative elements if not already set
|
|
406
|
+
const negatives = enhancer.getNegativeElements();
|
|
407
|
+
if (negatives && !enhancedNegativePrompt) {
|
|
408
|
+
enhancedNegativePrompt = negatives;
|
|
409
|
+
}
|
|
403
410
|
}
|
|
404
411
|
}
|
|
405
412
|
}
|
|
413
|
+
|
|
414
|
+
const instances: any[] = [
|
|
415
|
+
{
|
|
416
|
+
prompt: enhancedPrompt,
|
|
417
|
+
...(imagePart || {}),
|
|
418
|
+
...(lastFramePart || {}),
|
|
419
|
+
...(referenceImages ? { referenceImages } : {}),
|
|
420
|
+
},
|
|
421
|
+
];
|
|
422
|
+
|
|
423
|
+
const parameters: any = {
|
|
424
|
+
aspectRatio: args.aspect_ratio || "9:16",
|
|
425
|
+
durationSeconds: durationSeconds,
|
|
426
|
+
resolution: args.resolution || "720p",
|
|
427
|
+
negativePrompt: enhancedNegativePrompt,
|
|
428
|
+
generateAudio: args.generate_audio || false,
|
|
429
|
+
personGeneration,
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
const res = await fetch(url, {
|
|
433
|
+
method: "POST",
|
|
434
|
+
headers: {
|
|
435
|
+
Authorization: `Bearer ${token}`,
|
|
436
|
+
"Content-Type": "application/json",
|
|
437
|
+
},
|
|
438
|
+
body: JSON.stringify({ instances, parameters }),
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
if (!res.ok) {
|
|
442
|
+
const text = await res.text();
|
|
443
|
+
throw new Error(`Vertex request failed: ${res.status} ${text}`);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const op = (await res.json()) as any;
|
|
447
|
+
operationName = op.name || op.operation || "";
|
|
448
|
+
current = op;
|
|
406
449
|
}
|
|
407
450
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
const
|
|
418
|
-
aspectRatio: args.aspect_ratio || "9:16",
|
|
419
|
-
durationSeconds: durationSeconds,
|
|
420
|
-
resolution: args.resolution || "720p",
|
|
421
|
-
negativePrompt: enhancedNegativePrompt,
|
|
422
|
-
generateAudio: args.generate_audio || false,
|
|
423
|
-
personGeneration,
|
|
424
|
-
};
|
|
425
|
-
|
|
426
|
-
const res = await fetch(url, {
|
|
427
|
-
method: "POST",
|
|
428
|
-
headers: {
|
|
429
|
-
Authorization: `Bearer ${token}`,
|
|
430
|
-
"Content-Type": "application/json",
|
|
431
|
-
},
|
|
432
|
-
body: JSON.stringify({ instances, parameters }),
|
|
433
|
-
});
|
|
451
|
+
if (!operationName) {
|
|
452
|
+
throw new Error(
|
|
453
|
+
"Vertex did not return an operation name for long-running request",
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Construct the composite resume_endpoint: fetchUrl||operationName||outputPath
|
|
458
|
+
// This allows get_generation_status to use the URL directly and preserve output_path
|
|
459
|
+
const outputPathPart = args.output_path || "";
|
|
460
|
+
const compositeResumeEndpoint = `${fetchUrl}||${operationName}||${outputPathPart}`;
|
|
434
461
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
462
|
+
// Stream the resume_endpoint to the LLM immediately (before polling starts)
|
|
463
|
+
// This way the LLM has it even if MCP client times out during polling
|
|
464
|
+
if (context?.streamContent) {
|
|
465
|
+
const isResume = !!args.resume_endpoint;
|
|
466
|
+
await context.streamContent({
|
|
467
|
+
type: "text" as const,
|
|
468
|
+
text: isResume
|
|
469
|
+
? `[Vertex] Resuming status check for job`
|
|
470
|
+
: `[Vertex] Video generation started. resume_endpoint: ${compositeResumeEndpoint} (use this to check status if needed)`,
|
|
471
|
+
});
|
|
438
472
|
}
|
|
439
473
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
if (!operationName) {
|
|
446
|
-
throw new Error(
|
|
447
|
-
"Vertex did not return an operation name for long-running request",
|
|
448
|
-
);
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
// Construct the composite resume_endpoint: fetchUrl||operationName||outputPath
|
|
452
|
-
// This allows get_generation_status to use the URL directly and preserve output_path
|
|
453
|
-
const outputPathPart = args.output_path || "";
|
|
454
|
-
const compositeResumeEndpoint = `${fetchUrl}||${operationName}||${outputPathPart}`;
|
|
455
|
-
|
|
456
|
-
// Stream the resume_endpoint to the LLM immediately (before polling starts)
|
|
457
|
-
// This way the LLM has it even if MCP client times out during polling
|
|
458
|
-
if (context?.streamContent) {
|
|
459
|
-
const isResume = !!args.resume_endpoint;
|
|
460
|
-
await context.streamContent({
|
|
461
|
-
type: "text" as const,
|
|
462
|
-
text: isResume
|
|
463
|
-
? `[Vertex] Resuming status check for job`
|
|
464
|
-
: `[Vertex] Video generation started. resume_endpoint: ${compositeResumeEndpoint} (use this to check status if needed)`,
|
|
465
|
-
});
|
|
466
|
-
}
|
|
474
|
+
// Poll for status - keep polling until done
|
|
475
|
+
// Resume_endpoint was already streamed, so if MCP client times out the LLM still has it
|
|
476
|
+
let done = current ? !!current.done || !!current.response : false;
|
|
477
|
+
const startTime = Date.now();
|
|
478
|
+
const MAX_POLL_TIME = 60000; // 60 seconds internal timeout - then return resume_endpoint
|
|
467
479
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
let done = current ? !!current.done || !!current.response : false;
|
|
471
|
-
const startTime = Date.now();
|
|
472
|
-
const MAX_POLL_TIME = 60000; // 60 seconds internal timeout - then return resume_endpoint
|
|
480
|
+
while (!done && Date.now() - startTime < MAX_POLL_TIME) {
|
|
481
|
+
await wait(10000); // 10 second intervals
|
|
473
482
|
|
|
474
|
-
|
|
475
|
-
|
|
483
|
+
current = await checkVertexStatus(compositeResumeEndpoint);
|
|
484
|
+
done = !!current.done || !!current.response;
|
|
476
485
|
|
|
477
|
-
|
|
478
|
-
|
|
486
|
+
if (context?.reportProgress) {
|
|
487
|
+
const elapsed = Date.now() - startTime;
|
|
488
|
+
const progressPercent = Math.min(
|
|
489
|
+
Math.round((elapsed / MAX_POLL_TIME) * 100),
|
|
490
|
+
99,
|
|
491
|
+
);
|
|
492
|
+
await context.reportProgress({
|
|
493
|
+
progress: progressPercent,
|
|
494
|
+
total: 100,
|
|
495
|
+
});
|
|
496
|
+
}
|
|
479
497
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
498
|
+
if (context?.streamContent && !done) {
|
|
499
|
+
await context.streamContent({
|
|
500
|
+
type: "text" as const,
|
|
501
|
+
text: `[Vertex] Still processing... (${Math.round(
|
|
502
|
+
(Date.now() - startTime) / 1000,
|
|
503
|
+
)}s elapsed)`,
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
if (!done) {
|
|
509
|
+
return JSON.stringify({
|
|
510
|
+
status: "IN_PROGRESS",
|
|
511
|
+
request_id: operationName,
|
|
512
|
+
resume_endpoint: compositeResumeEndpoint,
|
|
513
|
+
message:
|
|
514
|
+
"Still in progress. Call this tool again with resume_endpoint to continue checking.",
|
|
489
515
|
});
|
|
490
516
|
}
|
|
491
517
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
518
|
+
const resp = current.response || current;
|
|
519
|
+
|
|
520
|
+
// checkVertexStatus already handles saving videos and sanitizing base64
|
|
521
|
+
if (Array.isArray(resp.saved_videos) && resp.saved_videos.length > 0) {
|
|
522
|
+
return JSON.stringify({
|
|
523
|
+
videos: resp.saved_videos,
|
|
524
|
+
message: "Video(s) generated successfully",
|
|
498
525
|
});
|
|
499
526
|
}
|
|
500
|
-
}
|
|
501
527
|
|
|
502
|
-
|
|
528
|
+
// If nothing saved, return a clean error without any raw JSON that could contain base64
|
|
529
|
+
// CRITICAL: Never return raw response data to prevent context window poisoning
|
|
530
|
+
const respKeys = resp ? Object.keys(resp) : [];
|
|
503
531
|
return JSON.stringify({
|
|
504
|
-
status: "
|
|
505
|
-
request_id: operationName,
|
|
506
|
-
resume_endpoint: compositeResumeEndpoint,
|
|
532
|
+
status: "ERROR",
|
|
507
533
|
message:
|
|
508
|
-
"
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
const resp = current.response || current;
|
|
513
|
-
|
|
514
|
-
// checkVertexStatus already handles saving videos and sanitizing base64
|
|
515
|
-
if (Array.isArray(resp.saved_videos) && resp.saved_videos.length > 0) {
|
|
516
|
-
return JSON.stringify({
|
|
517
|
-
videos: resp.saved_videos,
|
|
518
|
-
message: "Video(s) generated successfully",
|
|
534
|
+
"Vertex operation completed but no videos were found in the response.",
|
|
535
|
+
operationName,
|
|
536
|
+
responseKeys: respKeys,
|
|
537
|
+
hint: "The response structure may have changed. Check the Vertex AI documentation or search for the expected response format.",
|
|
519
538
|
});
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
const respKeys = resp ? Object.keys(resp) : [];
|
|
525
|
-
return JSON.stringify({
|
|
526
|
-
status: "ERROR",
|
|
527
|
-
message:
|
|
528
|
-
"Vertex operation completed but no videos were found in the response.",
|
|
529
|
-
operationName,
|
|
530
|
-
responseKeys: respKeys,
|
|
531
|
-
hint: "The response structure may have changed. Check the Vertex AI documentation or search for the expected response format.",
|
|
532
|
-
});
|
|
533
|
-
}, "imageToVideo");
|
|
539
|
+
},
|
|
540
|
+
"imageToVideo",
|
|
541
|
+
{ toolName: "generateVideoi2v" },
|
|
542
|
+
);
|
|
534
543
|
},
|
|
535
544
|
};
|