Agregar un Agente de Imagen IA a Mastra con Kie.ai
Guia paso a paso para agregar un agente de generacion de imagenes Mastra impulsado por Kie.ai. Cubre Nano Banana, Flux-2, GPT Image, Seedream, subidas, trabajos async y descargas locales para portadas de blog y miniaturas de YouTube.

Ya tengo un asistente Mastra que puede buscar en la web, leer archivos, ejecutar comandos shell y recordar contexto. Eso cubre investigacion y codificacion. No cubre la otra mitad del trabajo de contenido: portadas, miniaturas, graficos sociales y ediciones con logo.
Asi que agregue un agente de imagen dedicado. Chateas con el en Mastra Studio, elige un modelo, escribe un prompt, llama a una API de imagen, hace poll hasta que el trabajo termina y guarda el PNG en el workspace. Sin pestana de diseno separada, sin copiar y pegar URLs temporales que desaparecen dos semanas despues.
El backend de generacion es Kie.ai. Una API key, un formato de trabajo async y acceso a los modelos que realmente uso para portadas de blog y miniaturas de YouTube: Google Nano Banana / Nano Banana 2, Seedream, Flux-2, GPT Image 2, Ideogram, Topaz upscale, Recraft background removal y mas.
Este articulo recorre como conecte ese agente en Mastra, desde el cliente Kie hasta las instrucciones del agente. Puedes dejar caer el mismo patron en el proyecto mastra-assistant o cualquier aplicacion Mastra existente.
Prueba Kie.ai (API Key) Construye un Agente Mastra PrimeroPor que Kie.ai para generacion de imagenes
Si solo necesitas el modelo de imagen de OpenAI, llama a OpenAI. El dolor empieza cuando quieres varios modelos detras de una integracion:
- Google Nano Banana 2 para portadas 16:9 rapidas y de alta calidad
- Seedream Pro para tomas fotorrealistas estilo producto
- Flux-2 Pro para graficos comerciales nitidos
- GPT Image 2 cuando importa seguir instrucciones y texto en imagen
- Ideogram cuando la tipografia tiene que ser legible
- Topaz / Recraft para upscale y eliminacion de fondo
Cada proveedor oficial tiene su propia auth, formato de payload, historia de polling y pagina de precios. Kie se situa en el medio como una capa API multimodelo: modelos de imagen, video, musica y LLM detras de una billetera y una API de tareas.
Aqui esta la imagen practica de su plataforma y documentacion:
| Caracteristica | Lo que obtienes |
|---|---|
| Modelos | 100+ entre imagen, video, audio y chat (Nano Banana, GPT Image, Seedream, Flux, Veo, Kling, Seedance, Suno, Claude, Gemini, GPT y mas) |
| Precios | Generalmente ~30-50% por debajo de APIs oficiales; algunos modelos de alta demanda mucho mas bajos (la plataforma reclama hasta ~80% en modelos seleccionados) |
| Facturacion | Billetera de creditos; la plataforma indica que generaciones fallidas no se cobran |
| Creditos | No expiran; cuentas nuevas obtienen creditos de prueba gratuitos para probar en el Playground |
| Auth | Authorization: Bearer YOUR_API_KEY contra https://api.kie.ai |
| Trabajos | Async: POST /api/v1/jobs/createTask → poll GET /api/v1/jobs/recordInfo?taskId=... o usa un webhook |
| Limites de tasa | Aproximadamente 20 nuevas solicitudes / 10 segundos por defecto; tareas concurrentes en ejecucion son generosas |
| Retencion | Medios generados ~14 dias; logs ~2 meses (descarga lo que te importe) |
| Ops | UI de logs, limites de tasa por API key, lista IP allowlist opcional, canales de soporte Discord/Telegram |
Para trabajo de agentes, el modelo de trabajo async es la parte importante. La generacion de imagenes no es un chat completion de 200ms. Tu herramienta debe crear una tarea, esperar (o hacer poll) y luego descargar el resultado antes de que la URL temporal de Kie caduque. Eso es exactamente lo que hacen las herramientas Mastra a continuacion.
Ejemplos de precios de imagen de su pagina publica de precios (verifica kie.ai/pricing para numeros actuales):
- Nano Banana 2: aproximadamente $0.04-$0.09 por imagen dependiendo de la resolucion
- GPT Image 2: aproximadamente $0.03-$0.08 por imagen en tamanos comunes
- APIs oficiales: generalmente 2-5x mas caras en el mismo nivel de modelo
Uso Kie porque puedo cambiar model ids sin reescribir el agente cada vez que llega un nuevo modelo de imagen de Google u OpenAI.
Abrir Kie.ai Market y Precios Leer Docs de Kie APIDivulgacion de afiliados
Algunos enlaces a Kie.ai en este articulo son enlaces de afiliados (go.bitdoze.com/kie-ai). Uso el producto para generacion de imagenes en mi propia configuracion Mastra. Los precios y disponibilidad de modelos cambian; siempre verifica en el sitio oficial.
Que construiremos
Al final tendras:
- Un pequeno cliente Kie (crear tarea, poll, creditos, subida de archivos, descarga)
- Cinco herramientas Mastra: listar modelos, generar, estado de tarea, creditos, listar assets
- Un agente de imagen con instrucciones para miniaturas, portadas de blog y flujos de imagen de referencia
- Registro en
src/mastra/index.tspara que Studio muestreimage-agent
El agente puede:
- Generar assets text-to-image a 16:9 para blog/YouTube
- Editar con logos o referencias de rostros (modelos image-to-image / edit)
- Hacer upscale o eliminar fondos
- Guardar archivos bajo
workspace/images/generated/ - Investigar referencias visuales con TinyFish si ya tienes esas herramientas de la guia del asistente Mastra
Las subidas de referencia se manejan dentro de generate_kie_image via localImagePaths (subir archivo local → URL Kie → adjuntar a la tarea). No necesitas una herramienta de preparacion separada para el flujo basico.
Prerrequisitos
- Un proyecto Mastra funcionando (ver Construye Tu Propio Agente IA con Mastra)
- Node.js 22+ (o Bun, si asi ejecutas la app)
- Una API key de LLM para el cerebro del agente (OpenRouter, OpenCode Go, etc.)
- Una API key de Kie.ai
Opcional pero util: TinyFish para flujos de “investigar miniaturas de competidores antes de generar”.
Arquitectura
User (Studio chat)
│
▼
image-agent (LLM + instructions)
│
├─ list_kie_image_models
├─ generate_kie_image ──► Kie createTask + poll + download
│ (uploads localImagePaths first if needed)
├─ get_kie_image_task
├─ get_kie_credits
└─ list_image_assets
│
▼
workspace/images/uploads/ (your logos & refs)
workspace/images/generated/ (outputs)
El LLM nunca habla con Kie directamente. Solo elige herramientas. Eso mantiene auth, reintentos, seguridad de rutas y logica de descarga en TypeScript donde puedes probarlo.
Paso 1: Obtener una API key de Kie
- Registrate en Kie.ai
- Abre gestion de API Key
- Crea una key y guardala solo en env del servidor (nunca en codigo frontend)
Agrega a .env:
KIE_API_KEY=your-kie-api-key
Overrides opcionales:
# Usa un modelo mas fuerte/barato solo para el agente de imagen
AGENT_IMAGE_MODEL=google/gemini-2.5-flash
AGENT_IMAGE_MAX_STEPS=25
Paso 2: Conceptos basicos del cliente Kie
Crea src/mastra/tools/kie-client.ts. Esta es la capa de bajo nivel. Manten el catalogo de modelos y HTTP aqui para que las herramientas se mantengan delgadas.
Auth y bases
export const KIE_API_BASE = "https://api.kie.ai";
// File uploads use a separate host
export const KIE_UPLOAD_BASE = "https://kieai.redpandaai.co";
export function getApiKey(): string {
const apiKey = process.env.KIE_API_KEY;
if (!apiKey) {
throw new Error(
"KIE_API_KEY is not set. Add it to .env (from https://kie.ai/api-key).",
);
}
return apiKey;
}
function authHeaders(json = true): HeadersInit {
const headers: Record<string, string> = {
Authorization: `Bearer ${getApiKey()}`,
};
if (json) headers["Content-Type"] = "application/json";
return headers;
}
Crear tarea + poll
La API Market de Kie es async. Un 200 al crear significa “aceptado,” no “imagen lista.”
export async function kieCreateTask(body: {
model: string;
input: Record<string, unknown>;
callBackUrl?: string;
}): Promise<{ ok: true; data: { taskId: string } } | { ok: false; error: string }> {
const res = await fetch(`${KIE_API_BASE}/api/v1/jobs/createTask`, {
method: "POST",
headers: authHeaders(),
body: JSON.stringify(body),
});
const json = await res.json();
if (!res.ok || json?.code !== 200) {
return { ok: false, error: json?.msg || `createTask failed (HTTP ${res.status})` };
}
const taskId = json?.data?.taskId as string | undefined;
if (!taskId) return { ok: false, error: "createTask succeeded but no taskId returned" };
return { ok: true, data: { taskId } };
}
export async function kieGetTask(taskId: string) {
const res = await fetch(
`${KIE_API_BASE}/api/v1/jobs/recordInfo?taskId=${encodeURIComponent(taskId)}`,
{ method: "GET", headers: authHeaders(false) },
);
const json = await res.json();
const data = json?.data;
if (!data) return { ok: false as const, error: json?.msg || "No task data" };
let resultUrls: string[] | undefined;
if (data.resultJson) {
const parsed =
typeof data.resultJson === "string" ? JSON.parse(data.resultJson) : data.resultJson;
if (parsed?.resultUrls) resultUrls = parsed.resultUrls;
}
return {
ok: true as const,
data: {
taskId: data.taskId as string,
model: data.model as string | undefined,
// waiting | queuing | generating | success | fail
state: data.state as string | undefined,
resultUrls,
failMsg: data.failMsg as string | undefined,
creditsConsumed: data.creditsConsumed as number | undefined,
},
};
}
export async function pollTaskUntilDone(
taskId: string,
options?: { timeoutMs?: number; intervalMs?: number },
) {
const timeoutMs = options?.timeoutMs ?? 10 * 60 * 1000;
const intervalMs = options?.intervalMs ?? 3000;
const start = Date.now();
let delay = intervalMs;
while (Date.now() - start < timeoutMs) {
const result = await kieGetTask(taskId);
if (!result.ok) return result;
// Terminal states only. waiting / queuing / generating keep looping.
if (result.data.state === "success" || result.data.state === "fail") return result;
await new Promise((r) => setTimeout(r, delay));
delay = Math.min(delay * 1.25, 15000);
}
return { ok: false as const, error: `Timed out waiting for task ${taskId}` };
}
Creditos
export async function kieGetCredits() {
const res = await fetch(`${KIE_API_BASE}/api/v1/chat/credit`, {
method: "GET",
headers: authHeaders(false),
});
const json = await res.json();
if (!res.ok || json?.code !== 200) {
return { ok: false as const, error: json?.msg || "credit check failed" };
}
return { ok: true as const, data: Number(json.data) };
}
Subir archivos locales (para logos / referencias de rostros)
Los modelos I2I y edit necesitan URLs de imagen publicas o alojadas en Kie. Sube archivos locales del workspace primero:
export async function kieUploadLocalFile(absPath: string) {
const buf = await Bun.file(absPath).arrayBuffer(); // or fs.readFileSync
const bytes = Buffer.from(buf);
const ext = absPath.split(".").pop()?.toLowerCase() ?? "png";
const mime =
ext === "png" ? "image/png" : ext === "webp" ? "image/webp" : "image/jpeg";
const base64Data = `data:${mime};base64,${bytes.toString("base64")}`;
const res = await fetch(`${KIE_UPLOAD_BASE}/api/file-base64-upload`, {
method: "POST",
headers: authHeaders(),
body: JSON.stringify({
base64Data,
uploadPath: "images",
fileName: absPath.split("/").pop(),
}),
});
const json = await res.json();
// Docs return downloadUrl and fileUrl
const fileUrl =
json?.data?.downloadUrl || json?.data?.fileUrl || json?.downloadUrl;
if (!fileUrl) {
return { ok: false as const, error: json?.msg || "Upload returned no URL" };
}
return { ok: true as const, data: { fileUrl: String(fileUrl) } };
}
Los archivos subidos en el host de Kie son temporales (docs: eliminados despues de unos dias; trata las URLs como de corta duracion). Prefiere downloadUrl o fileUrl de la respuesta y pasalos a la tarea de generacion de inmediato.
Descargar resultados al workspace
Los medios de Kie son temporales (~14 dias; algunos docs tambien indican que las URLs de resultado pueden caducar mas rapido). Siempre guarda en disco.
import { join } from "node:path";
import { mkdirSync, writeFileSync } from "node:fs";
export const IMAGES_GENERATED = join(process.cwd(), "workspace/images/generated");
export const IMAGES_UPLOADS = join(process.cwd(), "workspace/images/uploads");
export function resolveWorkspacePath(relativeOrAbs: string): string {
if (relativeOrAbs.startsWith("/") || /^[A-Za-z]:\\/.test(relativeOrAbs)) {
return relativeOrAbs;
}
// Accept "images/uploads/logo.png" or "workspace/images/uploads/logo.png"
const cleaned = relativeOrAbs.replace(/^workspace\//, "");
return join(process.cwd(), "workspace", cleaned);
}
export function findModel(modelId: string) {
return KIE_IMAGE_MODELS.find((m) => m.id === modelId);
}
export async function downloadToGenerated(url: string, fileName?: string) {
mkdirSync(IMAGES_GENERATED, { recursive: true });
const res = await fetch(url);
if (!res.ok) throw new Error(`Download failed HTTP ${res.status}`);
const buf = Buffer.from(await res.arrayBuffer());
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
const base = (fileName ?? `kie-${stamp}`).replace(/[^a-zA-Z0-9._-]/g, "_");
const withExt = base.includes(".") ? base : `${base}.png`;
const outPath = join(IMAGES_GENERATED, withExt);
writeFileSync(outPath, buf);
return { path: outPath, bytesWritten: buf.length };
}
Catalogo de modelos curado
No dejes que el LLM invente model ids. Incluye una lista curada que el agente pueda listar y filtrar:
export type ImageMode =
| "text-to-image"
| "image-to-image"
| "edit"
| "upscale"
| "remove-background";
export type KieImageModel = {
id: string;
name: string;
family: string;
mode: ImageMode;
bestFor: string;
supportsImageUrls: boolean;
/** Kie input field for reference images (varies by model) */
imageField?: "image_input" | "image_urls" | "input_urls" | "image";
defaultAspectRatio?: string;
notes?: string;
};
export const KIE_IMAGE_MODELS: KieImageModel[] = [
{
id: "nano-banana-2",
name: "Google Nano Banana 2",
family: "Google",
mode: "text-to-image",
bestFor: "Blog covers, social graphics, optional image refs",
supportsImageUrls: true,
imageField: "image_input",
defaultAspectRatio: "16:9",
notes: "Bare id nano-banana-2 (not google/nano-banana-2). resolution: 1K|2K|4K",
},
{
id: "seedream/5-pro-text-to-image",
name: "Seedream 5.0 Pro T2I",
family: "Seedream",
mode: "text-to-image",
bestFor: "Photoreal thumbnails and product shots",
supportsImageUrls: false,
defaultAspectRatio: "16:9",
notes: "Requires quality: basic|high in addition to aspect_ratio",
},
{
id: "flux-2/pro-text-to-image",
name: "Flux-2 Pro T2I",
family: "Flux-2",
mode: "text-to-image",
bestFor: "Sharp commercial graphics",
supportsImageUrls: false,
defaultAspectRatio: "16:9",
},
{
id: "gpt-image-2-text-to-image",
name: "GPT Image 2 T2I",
family: "GPT Image",
mode: "text-to-image",
bestFor: "Instruction following, text-in-image",
supportsImageUrls: false,
defaultAspectRatio: "16:9",
},
{
id: "google/nano-banana-edit",
name: "Google Nano Banana Edit",
family: "Google",
mode: "edit",
bestFor: "Logo-aware edits and photo instructions",
supportsImageUrls: true,
imageField: "image_urls",
defaultAspectRatio: "16:9",
},
{
id: "gpt-image-2-image-to-image",
name: "GPT Image 2 I2I",
family: "GPT Image",
mode: "image-to-image",
bestFor: "Face lock / identity-preserving edits",
supportsImageUrls: true,
imageField: "input_urls",
defaultAspectRatio: "16:9",
},
{
id: "topaz/image-upscale",
name: "Topaz Image Upscale",
family: "Topaz",
mode: "upscale",
bestFor: "Final 2x/4x upscale",
supportsImageUrls: true,
imageField: "image_urls",
},
{
id: "recraft/remove-background",
name: "Recraft Remove Background",
family: "Recraft",
mode: "remove-background",
bestFor: "Cut out product/logo backgrounds",
supportsImageUrls: true,
imageField: "image",
},
];
Agrega mas ids desde docs.kie.ai segun los necesites. El agente solo ve lo que listas.
Paso 3: Herramientas de imagen Mastra
Crea src/mastra/tools/kie-image.ts. Las herramientas son createTool + Zod. El campo description es la documentacion API orientada al modelo, asi que escribelo con cuidado.
Listar modelos
import { createTool } from "@mastra/core/tools";
import { z } from "zod";
import { KIE_IMAGE_MODELS } from "./kie-client";
export const listKieImageModels = createTool({
id: "list_kie_image_models",
description:
"List available Kie.ai image models for thumbnails, blog covers, and marketing graphics. Call this before generating if you are unsure which model id to use.",
inputSchema: z.object({
mode: z
.enum([
"text-to-image",
"image-to-image",
"edit",
"upscale",
"remove-background",
"all",
])
.optional(),
family: z.string().optional(),
}),
outputSchema: z.object({
count: z.number(),
models: z.array(
z.object({
id: z.string(),
name: z.string(),
family: z.string(),
mode: z.string(),
bestFor: z.string(),
supportsImageUrls: z.boolean(),
notes: z.string().optional(),
}),
),
}),
execute: async (input) => {
let models = KIE_IMAGE_MODELS;
if (input.mode && input.mode !== "all") {
models = models.filter((m) => m.mode === input.mode);
}
if (input.family) {
const f = input.family.toLowerCase();
models = models.filter((m) => m.family.toLowerCase().includes(f));
}
return {
count: models.length,
models: models.map((m) => ({
id: m.id,
name: m.name,
family: m.family,
mode: m.mode,
bestFor: m.bestFor,
supportsImageUrls: m.supportsImageUrls,
notes: m.notes,
})),
};
},
});
Generar (crear + poll + descargar)
import { existsSync } from "node:fs";
import {
findModel,
kieCreateTask,
pollTaskUntilDone,
kieUploadLocalFile,
downloadToGenerated,
getApiKey,
resolveWorkspacePath,
} from "./kie-client";
export const generateKieImage = createTool({
id: "generate_kie_image",
description: `Generate or edit an image via Kie.ai. Creates an async task, polls until done, downloads into workspace/images/generated/, returns local paths.
Defaults:
- Blog/YouTube 16:9: nano-banana-2, seedream/5-pro-text-to-image, flux-2/pro-text-to-image
- Text-heavy graphics: gpt-image-2-text-to-image or ideogram/v3-text-to-image
- Logo/reference edits: google/nano-banana-edit or nano-banana-2 with localImagePaths
Important: Nano Banana 2 ids are bare: nano-banana-2 (NOT google/nano-banana-2).`,
inputSchema: z.object({
model: z.string().describe("Exact Market model id"),
prompt: z.string().min(1).max(20000).optional(),
aspectRatio: z.string().optional().describe("e.g. 16:9, 1:1, 9:16"),
quality: z.string().optional().describe("Seedream: basic|high"),
resolution: z.string().optional().describe("1K | 2K | 4K when supported"),
imageUrls: z.array(z.string().url()).optional(),
localImagePaths: z
.array(z.string())
.optional()
.describe("Workspace paths under images/uploads/, uploaded to Kie first"),
fileName: z.string().optional(),
wait: z.boolean().optional().describe("Default true: poll + download"),
}),
outputSchema: z.object({
success: z.boolean(),
taskId: z.string().optional(),
model: z.string().optional(),
state: z.string().optional(),
remoteUrls: z.array(z.string()).optional(),
localPaths: z.array(z.string()).optional(),
creditsConsumed: z.number().optional(),
error: z.string().optional(),
}),
execute: async (input) => {
try {
getApiKey();
} catch (e) {
return { success: false, error: e instanceof Error ? e.message : String(e) };
}
const imageUrls: string[] = [...(input.imageUrls ?? [])];
if (input.localImagePaths?.length) {
for (const p of input.localImagePaths) {
const abs = resolveWorkspacePath(p);
if (!existsSync(abs)) {
return {
success: false,
error: `localImagePath not found: ${p}. Do not generate without the reference.`,
};
}
const uploaded = await kieUploadLocalFile(abs);
if (!uploaded.ok) {
return { success: false, error: `Upload failed for ${p}: ${uploaded.error}` };
}
imageUrls.push(uploaded.data.fileUrl);
}
}
const meta = findModel(input.model);
const taskInput: Record<string, unknown> = {
prompt: input.prompt,
aspect_ratio: input.aspectRatio ?? meta?.defaultAspectRatio ?? "16:9",
};
if (input.quality) taskInput.quality = input.quality;
if (input.resolution) taskInput.resolution = input.resolution;
// Seedream Pro requires quality even if the agent forgets it
if (input.model.startsWith("seedream/") && !taskInput.quality) {
taskInput.quality = "basic";
}
if (imageUrls.length) {
// Field name is model-specific: image_input | image_urls | input_urls | image
const field =
meta?.imageField ??
(meta?.id === "nano-banana-2" || meta?.id === "nano-banana-pro"
? "image_input"
: meta?.id?.startsWith("gpt-image")
? "input_urls"
: "image_urls");
if (field === "image") {
taskInput.image = imageUrls[0];
} else {
taskInput[field] = imageUrls;
}
}
const created = await kieCreateTask({ model: input.model, input: taskInput });
if (!created.ok) return { success: false, error: created.error, model: input.model };
const taskId = created.data.taskId;
if (input.wait === false) {
return { success: true, taskId, model: input.model, state: "submitted" };
}
const done = await pollTaskUntilDone(taskId);
if (!done.ok) return { success: false, taskId, error: done.error, model: input.model };
if (done.data.state === "fail") {
return {
success: false,
taskId,
state: "fail",
error: done.data.failMsg || "Generation failed",
model: input.model,
};
}
const remoteUrls = done.data.resultUrls ?? [];
const localPaths: string[] = [];
for (let i = 0; i < remoteUrls.length; i++) {
const base =
input.fileName && remoteUrls.length === 1
? input.fileName
: input.fileName
? `${input.fileName}-${i + 1}`
: undefined;
const saved = await downloadToGenerated(remoteUrls[i], base);
localPaths.push(saved.path);
}
return {
success: true,
taskId,
model: input.model,
state: done.data.state,
remoteUrls,
localPaths,
creditsConsumed: done.data.creditsConsumed,
};
},
});
Creditos, estado de tarea y assets locales
Manten estas pequenas. El agente las usa para “tengo presupuesto?” y “donde esta mi logo?”
export const getKieCredits = createTool({
id: "get_kie_credits",
description: "Check remaining Kie.ai account credits before bulk generation.",
inputSchema: z.object({}),
outputSchema: z.object({
success: z.boolean(),
credits: z.number().optional(),
error: z.string().optional(),
}),
execute: async () => {
try {
getApiKey();
} catch (e) {
return { success: false, error: e instanceof Error ? e.message : String(e) };
}
const result = await kieGetCredits();
if (!result.ok) return { success: false, error: result.error };
return { success: true, credits: result.data };
},
});
export const getKieImageTask = createTool({
id: "get_kie_image_task",
description:
"Check status of a Kie image task by taskId. When success and download=true, save files under images/generated/.",
inputSchema: z.object({
taskId: z.string(),
download: z.boolean().optional(),
fileName: z.string().optional(),
}),
outputSchema: z.object({
success: z.boolean(),
taskId: z.string().optional(),
state: z.string().optional(),
remoteUrls: z.array(z.string()).optional(),
localPaths: z.array(z.string()).optional(),
error: z.string().optional(),
}),
execute: async (input) => {
const result = await kieGetTask(input.taskId);
if (!result.ok) return { success: false, taskId: input.taskId, error: result.error };
const remoteUrls = result.data.resultUrls ?? [];
const localPaths: string[] = [];
if (result.data.state === "success" && input.download !== false) {
for (let i = 0; i < remoteUrls.length; i++) {
const base =
input.fileName && remoteUrls.length === 1
? input.fileName
: input.fileName
? `${input.fileName}-${i + 1}`
: undefined;
const saved = await downloadToGenerated(remoteUrls[i], base);
localPaths.push(saved.path);
}
}
if (result.data.state === "fail") {
return {
success: false,
taskId: input.taskId,
state: "fail",
error: result.data.failMsg || "Task failed",
};
}
return {
success: true,
taskId: input.taskId,
state: result.data.state,
remoteUrls,
localPaths: localPaths.length ? localPaths : undefined,
};
},
});
export const listImageAssets = createTool({
id: "list_image_assets",
description:
"List local images in workspace/images/uploads and workspace/images/generated. Use relative paths as localImagePaths.",
inputSchema: z.object({
subdir: z.enum(["uploads", "generated", "all"]).optional(),
}),
outputSchema: z.object({
count: z.number(),
assets: z.array(
z.object({
relativePath: z.string(),
size: z.number(),
modifiedAt: z.string(),
}),
),
}),
execute: async (input) => {
const { readdirSync, statSync, existsSync } = await import("node:fs");
const { join } = await import("node:path");
const dirs =
input.subdir === "uploads"
? [IMAGES_UPLOADS]
: input.subdir === "generated"
? [IMAGES_GENERATED]
: [IMAGES_UPLOADS, IMAGES_GENERATED];
const assets: { relativePath: string; size: number; modifiedAt: string }[] = [];
for (const dir of dirs) {
if (!existsSync(dir)) continue;
for (const name of readdirSync(dir)) {
if (name.startsWith(".")) continue;
const full = join(dir, name);
const st = statSync(full);
if (!st.isFile()) continue;
assets.push({
relativePath: full.includes("/workspace/")
? full.slice(full.indexOf("images/"))
: full,
size: st.size,
modifiedAt: st.mtime.toISOString(),
});
}
}
return { count: assets.length, assets };
},
});
Conecta las exportaciones:
export const kieImageTools = {
listKieImageModels,
generateKieImage,
getKieImageTask,
getKieCredits,
listImageAssets,
};
No saltes las subidas de referencia
Si el usuario proporciona una ruta de rostro o logo, subela y pasala a un modelo I2I/edit. Nunca vuelvas a puro text-to-image e inventes el rostro. Falla la llamada a la herramienta en su lugar.
Paso 4: Definir el agente de imagen
Crea src/mastra/agents/image-agent.ts. Un agente, instrucciones enfocadas, herramientas solo para imagenes (mas investigacion web opcional).
import { Agent } from "@mastra/core/agent";
import {
UnicodeNormalizer,
PromptInjectionDetector,
} from "@mastra/core/processors";
import { memory } from "../memory";
import { workspace } from "../workspaces";
import {
listKieImageModels,
generateKieImage,
getKieImageTask,
getKieCredits,
listImageAssets,
} from "../tools/kie-image";
const IMAGE_AGENT_MODEL =
process.env.AGENT_IMAGE_MODEL ??
process.env.AGENT_MODEL ??
"google/gemini-2.5-flash";
export const imageAgent = new Agent({
id: "image-agent",
name: "Image Agent",
instructions: () => {
const iso = new Date().toISOString().split("T")[0];
const year = String(new Date().getUTCFullYear());
return `TODAY IS ${iso}. THE CURRENT YEAR IS ${year}.
You are the Image Agent for YouTube thumbnails, blog covers, social graphics, product shots, and brand-aware edits.
## Capabilities
- List models with list_kie_image_models
- Generate/edit with generate_kie_image (async create + poll + download)
- Manage assets under images/uploads/ and images/generated/
- Check credits with get_kie_credits before bulk runs
## Workspace layout
- images/uploads/: logos, references, user sources
- images/generated/: Kie outputs
## Model defaults
| Use case | Preferred model |
|---|---|
| YouTube / blog 16:9 | nano-banana-2, seedream/5-pro-text-to-image, flux-2/pro-text-to-image |
| Fast draft | nano-banana-2-lite or seedream/5-lite-text-to-image |
| Readable text | gpt-image-2-text-to-image or ideogram/v3-text-to-image |
| Edit with logo/ref | google/nano-banana-edit, nano-banana-2 (+ localImagePaths) |
| Face lock | gpt-image-2-image-to-image (+ localImagePaths) |
| Upscale | topaz/image-upscale |
| Remove background | recraft/remove-background |
### Critical model IDs
- nano-banana-2, nano-banana-2-lite, nano-banana-pro: bare IDs (NOT google/nano-banana-2*)
- google/nano-banana, google/nano-banana-edit, google/imagen4: keep the google/ prefix
- Call list_kie_image_models when unsure; never invent prefixes
## Workflow
1. Clarify goal briefly (platform, aspect ratio, style). Prefer defaults: blog/YouTube → 16:9.
2. If logos/refs are needed, list_image_assets or ask the user to drop files in images/uploads/.
3. Write a strong prompt: subject, composition, lighting, style, palette, exact text, negatives.
4. Call generate_kie_image with a clear fileName.
5. Report local paths under images/generated/ and offer iteration.
## Prompt craft
- Be specific: camera angle, lighting, materials, mood, brand hex colors.
- Thumbnails: high contrast, large subject, 3–6 words of text, safe margins.
- Blog covers: readable at small size, match site aesthetic, avoid clutter.
- When using a logo: use an edit/I2I model and describe placement.
## Rules
- Prefer real tool work over guessing model ids.
- Do not invent file paths. List assets first.
- Kie remote URLs expire; always rely on local downloads.
- If KIE_API_KEY is missing, say so clearly.
- Keep responses concise: model, prompt used, output paths.`;
},
model: IMAGE_AGENT_MODEL,
memory,
workspace,
inputProcessors: [
new UnicodeNormalizer({
stripControlChars: true,
collapseWhitespace: true,
}),
// optional: PromptInjectionDetector with a cheap guard model
],
tools: {
listKieImageModels,
generateKieImage,
getKieImageTask,
getKieCredits,
listImageAssets,
},
defaultOptions: { maxSteps: Number(process.env.AGENT_IMAGE_MAX_STEPS ?? 25) },
});
Algunas decisiones de diseno que importan:
instructionses una funcion para que la fecha se mantenga actual (mismo patron que el agente asistente).- Los valores por defecto del modelo viven en el prompt, no solo en tu cabeza. El LLM sigue una tabla de forma mas confiable que un vago “elige un buen modelo.”
workspacees opcional pero util para que el agente tambien pueda listar/leer archivos si habilitas las herramientas de workspace. Las herramientas dedicadas de list/generate son suficientes para una configuracion minima.maxSteps: 25es suficiente para list → generate → reintento opcional sin loops descontrolados.
Paso 5: Registrar el agente
En src/mastra/index.ts:
import { Mastra } from "@mastra/core/mastra";
import { assistant } from "./agents/assistant";
import { imageAgent } from "./agents/image-agent";
export const mastra = new Mastra({
agents: {
assistant,
imageAgent,
},
// storage, logger, server (same as your existing app)
});
Si usas flags de dominio (deploys mas ligeros), filtralo:
// ENABLE_IMAGE=false to skip registration
if (process.env.ENABLE_IMAGE !== "false") {
agents.imageAgent = imageAgent;
}
Paso 6: Ejecutar y probar
# .env must include:
# KIE_API_KEY=...
# OPENROUTER_API_KEY=... (or your LLM provider)
# AGENT_MODEL=google/gemini-2.5-flash
npm run dev
# or: bun run dev
Abre Studio en http://localhost:4111, selecciona Image Agent y prueba:
Create a 16:9 blog cover for an article about adding an image agent to Mastra.
Style: clean purple gradient tech illustration, subtle node graph, title text
"Mastra Image Agent", high contrast, no clutter. Save as mastra-image-cover.
Lo que deberias ver en los traces:
- Opcional
list_kie_image_modelso eleccion directa de modelo (nano-banana-2/ Seedream / Flux) generate_kie_imageconaspectRatio: "16:9"y un prompt largo- Poll hasta
state: success - Ruta local como
workspace/images/generated/mastra-image-cover.png
Flujo de logo / referencia de rostro
mkdir -p workspace/images/uploads
cp ~/Downloads/logo.png workspace/images/uploads/logo.png
Luego en el chat:
Use images/uploads/logo.png. Generate a YouTube thumbnail 16:9 for
"Self-Host Mastra on a VPS". Place the logo bottom-left, keep it crisp,
dark background, large bold title, high contrast.
El agente deberia pasar localImagePaths: ["images/uploads/logo.png"] a un modelo edit/I2I como google/nano-banana-edit o nano-banana-2, no inventar el logo desde texto.
Tabla rapida de modelos
| Objetivo | Model id | Notas |
|---|---|---|
| Portada de blog por defecto | nano-banana-2 |
Id bare; refs opcionales image_input; 1K/2K/4K |
| Fotorreal | seedream/5-pro-text-to-image |
Necesita quality: basic|high |
| Comercial nitido | flux-2/pro-text-to-image |
Graficos de marketing |
| Texto en imagen | gpt-image-2-text-to-image o ideogram/v3-text-to-image |
Tipografia |
| Editar / logo | google/nano-banana-edit |
Campo: image_urls |
| Bloqueo de rostro | gpt-image-2-image-to-image |
Campo: input_urls |
| Upscale | topaz/image-upscale |
Verifica docs para campos de factor |
| Recorte | recraft/remove-background |
A menudo un campo singular image |
Los model ids cambian conforme Kie incorpora nuevos lanzamientos. Trata tu catalogo en kie-client.ts como la fuente de verdad, y manten list_kie_image_models en el cinturon de herramientas.
Patrones de prompt que funcionan
Portada de blog (16:9)
Wide 16:9 blog hero image. Subject: abstract TypeScript agent nodes connected
by thin purple lines on a soft lavender gradient. Clean modern tech illustration,
flat with subtle depth, generous negative space on the right for title overlay.
No watermarks, no tiny unreadable text, no crowded UI mockups. High contrast.
Miniatura de YouTube (16:9)
YouTube thumbnail 16:9, high contrast, large central subject (developer at laptop
with glowing purple AI node graph). Bold 4-word title "Build Image Agents" in
thick white sans-serif with dark outline. Leave safe margins. Punchy colors,
shallow depth of field, no busy background.
Composicion con logo
Place the provided logo in the bottom-left corner, keep proportions, do not
distort or recolor the logo mark. Dark navy background, soft bokeh lights,
product photography lighting, 16:9.
Consejos de produccion
- Descarga todo. Kie mantiene medios ~14 dias. Tu workspace es el almacen duradero.
- Falla fuerte en refs faltantes. Si la subida falla, devuelve un error. El fallback silencioso a T2I arruina el trabajo de marca.
- Vigila los creditos. Llama
get_kie_creditsantes de ejecuciones por lotes. - Separa el agente del asistente. El trabajo de imagen quema steps y tokens en prompts largos. Un agente dedicado mantiene al asistente de codificacion enfocado.
- Limites de tasa. Por defecto es aproximadamente 20 nuevos trabajos por 10 segundos. Para portadas por lotes, pon una pequena pausa entre creaciones.
- Manten secretos en el servidor.
KIE_API_KEYsolo en.env/ secretos del host. Nunca lo envies al navegador. - Webhook opcional. Para modelos de video largos despues, prefiere
callBackUrlsobre polls largos. Las imagenes generalmente funcionan bien con polling de 3-15s.
Solucion de problemas
401 / “You do not have access permissions”
Header Authorization: Bearer ... faltante o incorrecto, o KIE_API_KEY vacio. Confirma la key en kie.ai/api-key y que el proceso realmente cargo .env.
Model not supported / invalid model
Id incorrecto. Nano Banana 2 es nano-banana-2, no google/nano-banana-2. Llama list_kie_image_models y copia el id exactamente de tu catalogo / docs.kie.ai.
Tarea atascada en waiting / queuing / generating
Aun ejecutandose. Sigue haciendo poll. Aumenta timeoutMs para modelos pesados. Verifica kie.ai/logs para el estado real y mensaje de fallo.
Subida exitosa pero sin fileUrl
Parsea downloadUrl asi como fileUrl. El host de subida de Kie puede retornar cualquiera de los dos campos dependiendo del endpoint.
La imagen se ve mal / el rostro no coincide
Usaste un modelo T2I puro sin referencias, o la subida nunca se adjunto. Confirma que localImagePaths se resolvio, la subida retorno una URL y el modelo soporta entradas de imagen (google/nano-banana-edit, nano-banana-2, gpt-image-2-image-to-image, Seedream I2I). Tambien verifica el nombre del campo de imagen: image_input vs image_urls vs input_urls.
HTTP 429
Alcanzaste el limite de tasa de creacion. Haz back off y reintenta. Contacta soporte de Kie si necesitas limites mas altos para volumen de produccion.
Tarea Seedream rechazada al crear
Seedream 5 Pro requiere quality (basic o high) y aspect_ratio. La herramienta de ejemplo por defecto pone quality en basic cuando el model id empieza con seedream/.
Donde encaja esto con el resto de Mastra
- Asistente base: Construye Tu Propio Agente IA con Mastra
- Eleccion de framework: Mastra vs Eve
- Herramientas de investigacion web: TinyFish para agentes IA
- Resena del producto Kie (precios, competidores, pros/contras): Kie.ai Review 2026
- Plataforma Kie: go.bitdoze.com/kie-ai · docs en docs.kie.ai
Una vez que la generacion de imagenes funciona, el mismo patron se extiende a video (Veo, Kling, Seedance en Kie): misma forma createTask/poll, timeouts mas largos, diferentes model ids. Mantengo eso como un agente de video separado para que los presupuestos de steps y las instrucciones se mantengan limpios.
Obtener una API Key de Kie.ai Guia del Asistente MastraSiguiente paso
Registra el agente de imagen junto a tu asistente, abre Studio y genera una portada en workspace/images/generated/. Si algo falla, verifica la pagina de logs de Kie y el string de error de la herramienta primero. La mayoria de los problemas son model ids incorrectos o KIE_API_KEY faltante.