@mixio-pro/kalaasetu-mcp 2.0.11-beta → 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.
@@ -179,357 +179,366 @@ export const imageToVideo = {
179
179
  };
180
180
  },
181
181
  ) {
182
- return safeToolExecute(async () => {
183
- const projectId = "mixio-pro";
184
- const location = "us-central1";
185
- const modelId = args.model_id || "veo-3.1-fast-generate-preview";
186
-
187
- // Validate and parse duration_seconds - snap to nearest 4, 6, or 8
188
- let durationSeconds = parseInt(args.duration_seconds || "6");
189
- if (isNaN(durationSeconds)) durationSeconds = 6;
190
-
191
- const validDurations = [4, 6, 8];
192
- // Find nearest valid duration
193
- durationSeconds = validDurations.reduce((prev, curr) => {
194
- return Math.abs(curr - durationSeconds) <
195
- Math.abs(prev - durationSeconds)
196
- ? curr
197
- : prev;
198
- });
199
-
200
- // Tie-breaking: if equidistant (e.g. 5), the reduce above keeps the first one (4) because < is strict.
201
- // However, user requested "nearest duration with the ceil", effectively meaning round up if equidistant.
202
- // Let's explicitly handle the equidistant cases or just use a custom finder.
203
- // 5 -> equidistant to 4 and 6. "With ceil" implies 6.
204
- // 7 -> equidistant to 6 and 8. "With ceil" implies 8.
205
-
206
- // Simpler logic for these specific values:
207
- if (
208
- durationSeconds === 4 &&
209
- parseInt(args.duration_seconds || "6") === 5
210
- ) {
211
- durationSeconds = 6;
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
- token = await getGoogleAccessToken();
229
- if (context?.streamContent) {
230
- await context.streamContent({
231
- type: "text" as const,
232
- text: `[Vertex] ✓ Authentication successful. Token acquired.`,
233
- });
214
+ if (
215
+ durationSeconds === 6 &&
216
+ parseInt(args.duration_seconds || "6") === 7
217
+ ) {
218
+ durationSeconds = 8;
234
219
  }
235
- } catch (authError: any) {
236
- const errorMsg = authError?.message || String(authError);
237
- if (context?.streamContent) {
238
- await context.streamContent({
239
- type: "text" as const,
240
- text: `[Vertex] Authentication FAILED: ${errorMsg}. Check GOOGLE_APPLICATION_CREDENTIALS or run 'gcloud auth application-default login'.`,
241
- });
242
- }
243
- throw new Error(`Google Cloud authentication failed: ${errorMsg}`);
244
- }
245
-
246
- const fetchUrl = `https://${location}-aiplatform.googleapis.com/v1/projects/${projectId}/locations/${location}/publishers/google/models/${modelId}:fetchPredictOperation`;
247
-
248
- // If resuming, reconstruct the full operation path from the UUID
249
- let operationName: string | undefined;
250
- if (args.resume_endpoint) {
251
- // Support both UUID-only and full path formats
252
- if (args.resume_endpoint.includes("/")) {
253
- operationName = args.resume_endpoint; // Already a full path
254
- } else {
255
- // Reconstruct full path from UUID
256
- operationName = `projects/${projectId}/locations/${location}/publishers/google/models/${modelId}/operations/${args.resume_endpoint}`;
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
- if (!operationName) {
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
- if (context?.streamContent) {
267
- await context.streamContent({
268
- type: "text" as const,
269
- text: `[Vertex] Submitting video generation request to Veo model: ${modelId}...`,
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
- const url = `https://${location}-aiplatform.googleapis.com/v1/projects/${projectId}/locations/${location}/publishers/google/models/${modelId}:predictLongRunning`;
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
- let imagePart: any = undefined;
276
- if (args.image_path) {
277
- const { data, mimeType } = await fileToBase64(args.image_path);
278
- imagePart = {
279
- image: {
280
- bytesBase64Encoded: data,
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
- let lastFramePart: any = undefined;
287
- if (args.last_frame_path) {
288
- const { data, mimeType } = await fileToBase64(args.last_frame_path);
289
- lastFramePart = {
290
- lastFrame: {
291
- bytesBase64Encoded: data,
292
- mimeType,
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
- let referenceImages: any[] | undefined = undefined;
298
- if (args.reference_images) {
299
- let refImages: string[];
300
- if (typeof args.reference_images === "string") {
301
- if (
302
- args.reference_images.startsWith("[") &&
303
- args.reference_images.endsWith("]")
304
- ) {
305
- try {
306
- refImages = JSON.parse(args.reference_images);
307
- } catch {
308
- throw new Error("Invalid reference_images format");
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
- refImages = [args.reference_images];
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
- if (refImages.length > 0) {
322
- referenceImages = await Promise.all(
323
- refImages.slice(0, 3).map(async (p) => {
324
- const { data, mimeType } = await fileToBase64(p);
325
- return {
326
- image: {
327
- bytesBase64Encoded: data,
328
- mimeType,
329
- },
330
- referenceType: "asset",
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
- // Apply prompt enhancement logic
342
- let enhancedPrompt = args.prompt;
343
- let enhancedNegativePrompt = args.negative_prompt;
340
+ const personGeneration =
341
+ args.person_generation ||
342
+ (args.image_path ? "allow_adult" : "allow_all");
344
343
 
345
- // Determine which preset to use
346
- let presetToUse = args.enhancer_preset;
344
+ // Apply prompt enhancement logic
345
+ let enhancedPrompt = args.prompt;
346
+ let enhancedNegativePrompt = args.negative_prompt;
347
347
 
348
- // If auto_enhance is true and no preset specified, default to 'veo'
349
- if (args.auto_enhance === true && !presetToUse) {
350
- presetToUse = "veo";
351
- }
348
+ // Determine which preset to use
349
+ let presetToUse = args.enhancer_preset;
352
350
 
353
- // Disable enhancement if auto_enhance is explicitly false
354
- if (args.auto_enhance === false) {
355
- presetToUse = undefined;
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
- if (presetToUse && args.prompt) {
359
- // Use LLM-based enhancement for 'veo' preset
360
- if (presetToUse === "veo") {
361
- const { enhancePromptWithLLM, isLLMEnhancerAvailable } =
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
- try {
373
- enhancedPrompt = await enhancePromptWithLLM(args.prompt, "veo");
374
- context?.log?.info(
375
- `LLM-enhanced prompt for Veo: "${args.prompt}" → "${enhancedPrompt}"`,
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] Prompt enhanced. Length: ${args.prompt.length} ${enhancedPrompt.length} chars`,
371
+ text: `[VEO] Enhancing prompt with Gemini for optimal Veo 3.1 generation...`,
382
372
  });
383
373
  }
384
- } catch (err: any) {
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
- `LLM enhancement failed, using original: ${err.message}`,
397
+ "GEMINI_API_KEY not set, skipping Veo LLM enhancement",
387
398
  );
388
399
  }
389
400
  } else {
390
- context?.log?.info(
391
- "GEMINI_API_KEY not set, skipping Veo LLM enhancement",
392
- );
393
- }
394
- } else {
395
- // Fall back to static string-based enhancement for other presets
396
- const enhancer = resolveEnhancer(presetToUse);
397
- if (enhancer.hasTransformations()) {
398
- enhancedPrompt = enhancer.enhance(args.prompt);
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
- const instances: any[] = [
409
- {
410
- prompt: enhancedPrompt,
411
- ...(imagePart || {}),
412
- ...(lastFramePart || {}),
413
- ...(referenceImages ? { referenceImages } : {}),
414
- },
415
- ];
416
-
417
- const parameters: any = {
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
- if (!res.ok) {
436
- const text = await res.text();
437
- throw new Error(`Vertex request failed: ${res.status} ${text}`);
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
- const op = (await res.json()) as any;
441
- operationName = op.name || op.operation || "";
442
- current = op;
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
- // Poll for status - keep polling until done
469
- // Resume_endpoint was already streamed, so if MCP client times out the LLM still has it
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
- while (!done && Date.now() - startTime < MAX_POLL_TIME) {
475
- await wait(10000); // 10 second intervals
483
+ current = await checkVertexStatus(compositeResumeEndpoint);
484
+ done = !!current.done || !!current.response;
476
485
 
477
- current = await checkVertexStatus(compositeResumeEndpoint);
478
- done = !!current.done || !!current.response;
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
- if (context?.reportProgress) {
481
- const elapsed = Date.now() - startTime;
482
- const progressPercent = Math.min(
483
- Math.round((elapsed / MAX_POLL_TIME) * 100),
484
- 99,
485
- );
486
- await context.reportProgress({
487
- progress: progressPercent,
488
- total: 100,
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
- if (context?.streamContent && !done) {
493
- await context.streamContent({
494
- type: "text" as const,
495
- text: `[Vertex] Still processing... (${Math.round(
496
- (Date.now() - startTime) / 1000,
497
- )}s elapsed)`,
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
- if (!done) {
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: "IN_PROGRESS",
505
- request_id: operationName,
506
- resume_endpoint: compositeResumeEndpoint,
532
+ status: "ERROR",
507
533
  message:
508
- "Still in progress. Call this tool again with resume_endpoint to continue checking.",
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
- // If nothing saved, return a clean error without any raw JSON that could contain base64
523
- // CRITICAL: Never return raw response data to prevent context window poisoning
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
  };