Quellcodebibliothek Statistik Leitseite products/Sources/formale Sprachen/Java/Openclaw/src/agents/tools/   (KI Agentensystem Version 22©)  Datei vom 26.3.2026 mit Größe 37 kB image not shown  

Quelle  image-generate-tool.test.ts

  Sprache: JAVA
 

Spracherkennung für: .ts vermutete Sprache: Unknown {[0] [0] [0]} [Methode: Schwerpunktbildung, einfache Gewichte, sechs Dimensionen]

import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";

let imageGenerationRuntime: typeof import("../../image-generation/runtime.js");
let imageOps: typeof import("../../media/image-ops.js");
let splitMediaFromOutput: typeof import("../../media/parse.js").splitMediaFromOutput;
let mediaStore: typeof import("../../media/store.js");
let webMedia: typeof import("../../media/web-media.js");
let createImageGenerateTool: typeof import("./image-generate-tool.js").createImageGenerateTool;
let resolveImageGenerationModelConfigForTool: typeof import("./image-generate-tool.js").resolveImageGenerationModelConfigForTool;

function stubImageGenerationProviders() {
  vi.spyOn(imageGenerationRuntime, "listRuntimeImageGenerationProviders").mockReturnValue([
    {
      id: "google",
      defaultModel: "gemini-3.1-flash-image-preview",
      models: ["gemini-3.1-flash-image-preview", "gemini-3-pro-image-preview"],
      capabilities: {
        generate: {
          maxCount: 4,
          supportsAspectRatio: true,
          supportsResolution: true,
        },
        edit: {
          enabled: true,
          maxInputImages: 5,
          supportsAspectRatio: true,
          supportsResolution: true,
        },
        geometry: {
          resolutions: ["1K", "2K", "4K"],
          aspectRatios: ["1:1", "16:9"],
        },
      },
      generateImage: vi.fn(async () => {
        throw new Error("not used");
      }),
    },
    {
      id: "openai",
      defaultModel: "gpt-image-1",
      models: ["gpt-image-1"],
      capabilities: {
        generate: {
          maxCount: 4,
          supportsSize: true,
          supportsAspectRatio: true,
        },
        edit: {
          enabled: false,
          maxInputImages: 0,
        },
        geometry: {
          sizes: ["1024x1024", "1024x1536", "1536x1024"],
          aspectRatios: ["1:1", "16:9"],
        },
      },
      generateImage: vi.fn(async () => {
        throw new Error("not used");
      }),
    },
  ]);
}

function requireImageGenerateTool(tool: ReturnType<typeof createImageGenerateTool>) {
  expect(tool).not.toBeNull();
  if (!tool) {
    throw new Error("expected image_generate tool");
  }
  return tool;
}

function ensureDefaultImageGenerationProvidersStubbed() {
  if (vi.isMockFunction(imageGenerationRuntime.listRuntimeImageGenerationProviders)) {
    return;
  }
  stubImageGenerationProviders();
}

function createToolWithPrimaryImageModel(
  primary: string,
  extra?: {
    agentDir?: string;
    workspaceDir?: string;
  },
) {
  ensureDefaultImageGenerationProvidersStubbed();
  return requireImageGenerateTool(
    createImageGenerateTool({
      config: {
        agents: {
          defaults: {
            imageGenerationModel: {
              primary,
            },
          },
        },
      },
      ...extra,
    }),
  );
}

function stubEditedImageFlow(params?: { width?: number; height?: number }) {
  const generateImage = vi.spyOn(imageGenerationRuntime, "generateImage").mockResolvedValue({
    provider: "google",
    model: "gemini-3-pro-image-preview",
    attempts: [],
    ignoredOverrides: [],
    images: [
      {
        buffer: Buffer.from("png-out"),
        mimeType: "image/png",
        fileName: "edited.png",
      },
    ],
  });
  vi.spyOn(webMedia, "loadWebMedia").mockResolvedValue({
    kind: "image",
    buffer: Buffer.from("input-image"),
    contentType: "image/png",
  });
  if (params?.width && params?.height) {
    vi.spyOn(imageOps, "getImageMetadata").mockResolvedValue({
      width: params.width,
      height: params.height,
    });
  }
  vi.spyOn(mediaStore, "saveMediaBuffer").mockResolvedValue({
    path: "/tmp/edited.png",
    id: "edited.png",
    size: 7,
    contentType: "image/png",
  });
  return generateImage;
}

function createFalEditProvider(params?: {
  maxInputImages?: number;
  supportsAspectRatio?: boolean;
  aspectRatios?: string[];
}) {
  return {
    id: "fal",
    defaultModel: "fal-ai/flux/dev",
    models: ["fal-ai/flux/dev", "fal-ai/flux/dev/image-to-image"],
    capabilities: {
      generate: {
        maxCount: 4,
        supportsSize: true,
        supportsAspectRatio: true,
        supportsResolution: true,
      },
      edit: {
        enabled: true,
        maxInputImages: params?.maxInputImages ?? 1,
        supportsSize: true,
        supportsAspectRatio: params?.supportsAspectRatio ?? false,
        supportsResolution: true,
      },
      ...(params?.aspectRatios
        ? {
            geometry: {
              aspectRatios: params.aspectRatios,
            },
          }
        : {}),
    },
    generateImage: vi.fn(async () => {
      throw new Error("not used");
    }),
  };
}

describe("createImageGenerateTool", () => {
  beforeAll(async () => {
    vi.doMock("../../secrets/provider-env-vars.js", async () => {
      const actual = await vi.importActual<typeof import("../../secrets/provider-env-vars.js")>(
        "../../secrets/provider-env-vars.js",
      );
      return {
        ...actual,
        getProviderEnvVars: (providerId: string) => {
          if (providerId === "google") {
            return ["GEMINI_API_KEY", "GOOGLE_API_KEY"];
          }
          if (providerId === "openai") {
            return ["OPENAI_API_KEY"];
          }
          return [];
        },
      };
    });
    imageGenerationRuntime = await import("../../image-generation/runtime.js");
    imageOps = await import("../../media/image-ops.js");
    ({ splitMediaFromOutput } = await import("../../media/parse.js"));
    mediaStore = await import("../../media/store.js");
    webMedia = await import("../../media/web-media.js");
    ({ createImageGenerateTool, resolveImageGenerationModelConfigForTool } =
      await import("./image-generate-tool.js"));
  });

  beforeEach(() => {
    vi.stubEnv("OPENAI_API_KEY", "");
    vi.stubEnv("OPENAI_API_KEYS", "");
    vi.stubEnv("GEMINI_API_KEY", "");
    vi.stubEnv("GEMINI_API_KEYS", "");
    vi.stubEnv("GOOGLE_API_KEY", "");
    vi.stubEnv("GOOGLE_API_KEYS", "");
  });

  afterEach(() => {
    vi.restoreAllMocks();
    vi.unstubAllEnvs();
  });

  it("returns null when no image-generation model can be inferred", () => {
    stubImageGenerationProviders();
    expect(createImageGenerateTool({ config: {} })).toBeNull();
  });

  it("matches image-generation providers across canonical provider aliases", () => {
    vi.spyOn(imageGenerationRuntime, "listRuntimeImageGenerationProviders").mockReturnValue([
      {
        id: "z.ai",
        aliases: ["z-ai"],
        defaultModel: "glm-4.5-image",
        models: ["glm-4.5-image"],
        capabilities: {
          generate: {
            maxCount: 4,
          },
          edit: {
            enabled: false,
            maxInputImages: 0,
          },
          geometry: {},
        },
        generateImage: vi.fn(async () => {
          throw new Error("not used");
        }),
      },
    ]);

    expect(
      createImageGenerateTool({
        config: {
          agents: {
            defaults: {
              imageGenerationModel: {
                primary: "z-ai/glm-4.5-image",
              },
            },
          },
        },
      }),
    ).not.toBeNull();
  });

  it("infers an OpenAI image-generation model from env-backed auth", () => {
    stubImageGenerationProviders();
    vi.stubEnv("OPENAI_API_KEY", "openai-test");

    expect(resolveImageGenerationModelConfigForTool({ cfg: {} })).toEqual({
      primary: "openai/gpt-image-1",
    });
    expect(createImageGenerateTool({ config: {} })).not.toBeNull();
  });

  it("infers the canonical OpenAI image model from provider readiness without explicit config", () => {
    const isConfigured = vi.fn(({ agentDir }: { agentDir?: string }) => agentDir === "/tmp/agent");
    vi.spyOn(imageGenerationRuntime, "listRuntimeImageGenerationProviders").mockReturnValue([
      {
        id: "openai",
        defaultModel: "gpt-image-2",
        models: ["gpt-image-2"],
        isConfigured,
        capabilities: {
          generate: {
            maxCount: 4,
            supportsSize: true,
          },
          edit: {
            enabled: true,
            maxInputImages: 5,
            supportsSize: true,
          },
          geometry: {
            sizes: ["1024x1024", "1536x1024", "1024x1536"],
          },
        },
        generateImage: vi.fn(async () => {
          throw new Error("not used");
        }),
      },
    ]);

    expect(
      resolveImageGenerationModelConfigForTool({
        cfg: {},
        agentDir: "/tmp/agent",
      }),
    ).toEqual({
      primary: "openai/gpt-image-2",
    });
    expect(createImageGenerateTool({ config: {}, agentDir: "/tmp/agent" })).not.toBeNull();
    expect(isConfigured).toHaveBeenCalledWith({
      cfg: {},
      agentDir: "/tmp/agent",
    });
  });

  it("prefers the primary model provider when multiple image providers have auth", () => {
    stubImageGenerationProviders();
    vi.stubEnv("OPENAI_API_KEY", "openai-test");
    vi.stubEnv("GEMINI_API_KEY", "gemini-test");

    expect(
      resolveImageGenerationModelConfigForTool({
        cfg: {
          agents: {
            defaults: {
              model: {
                primary: "google/gemini-3.1-pro-preview",
              },
            },
          },
        },
      }),
    ).toEqual({
      primary: "google/gemini-3.1-flash-image-preview",
      fallbacks: ["openai/gpt-image-1"],
    });
  });

  it("generates images and returns details.media paths", async () => {
    vi.spyOn(imageGenerationRuntime, "listRuntimeImageGenerationProviders").mockReturnValue([
      {
        id: "openai",
        defaultModel: "gpt-image-1",
        models: ["gpt-image-1"],
        capabilities: {
          generate: {
            maxCount: 4,
            supportsSize: true,
            supportsAspectRatio: true,
          },
          edit: {
            enabled: false,
            maxInputImages: 0,
          },
          geometry: {
            sizes: ["1024x1024", "1024x1536", "1536x1024"],
            aspectRatios: ["1:1", "16:9"],
          },
        },
        generateImage: vi.fn(async () => {
          throw new Error("not used");
        }),
      },
    ]);
    const generateImage = vi.spyOn(imageGenerationRuntime, "generateImage").mockResolvedValue({
      provider: "openai",
      model: "gpt-image-1",
      attempts: [],
      ignoredOverrides: [],
      images: [
        {
          buffer: Buffer.from("png-1"),
          mimeType: "image/png",
          fileName: "cat-one.png",
        },
        {
          buffer: Buffer.from("png-2"),
          mimeType: "image/png",
          fileName: "cat-two.png",
          revisedPrompt: "A more cinematic cat",
        },
      ],
    });
    const saveMediaBuffer = vi.spyOn(mediaStore, "saveMediaBuffer");
    saveMediaBuffer.mockResolvedValueOnce({
      path: "/tmp/generated-1.png",
      id: "generated-1.png",
      size: 5,
      contentType: "image/png",
    });
    saveMediaBuffer.mockResolvedValueOnce({
      path: "/tmp/generated-2.png",
      id: "generated-2.png",
      size: 5,
      contentType: "image/png",
    });

    const tool = createImageGenerateTool({
      config: {
        agents: {
          defaults: {
            mediaMaxMb: 8,
            imageGenerationModel: {
              primary: "openai/gpt-image-1",
            },
          },
        },
      },
      agentDir: "/tmp/agent",
    });

    expect(tool).not.toBeNull();
    if (!tool) {
      throw new Error("expected image_generate tool");
    }

    const result = await tool.execute("call-1", {
      prompt: "A cat wearing sunglasses",
      model: "openai/gpt-image-1",
      filename: "cats/output.png",
      count: 2,
      size: "1024x1024",
    });

    expect(generateImage).toHaveBeenCalledWith(
      expect.objectContaining({
        cfg: {
          agents: {
            defaults: {
              mediaMaxMb: 8,
              imageGenerationModel: {
                primary: "openai/gpt-image-1",
              },
            },
          },
        },
        prompt: "A cat wearing sunglasses",
        agentDir: "/tmp/agent",
        modelOverride: "openai/gpt-image-1",
        size: "1024x1024",
        count: 2,
        inputImages: [],
      }),
    );
    expect(saveMediaBuffer).toHaveBeenNthCalledWith(
      1,
      Buffer.from("png-1"),
      "image/png",
      "tool-image-generation",
      8 * 1024 * 1024,
      "cats/output.png",
    );
    expect(saveMediaBuffer).toHaveBeenNthCalledWith(
      2,
      Buffer.from("png-2"),
      "image/png",
      "tool-image-generation",
      8 * 1024 * 1024,
      "cats/output.png",
    );
    expect(result).toMatchObject({
      content: [
        {
          type: "text",
          text: expect.stringContaining("Generated 2 images with openai/gpt-image-1."),
        },
      ],
      details: {
        provider: "openai",
        model: "gpt-image-1",
        count: 2,
        media: {
          mediaUrls: ["/tmp/generated-1.png", "/tmp/generated-2.png"],
        },
        paths: ["/tmp/generated-1.png", "/tmp/generated-2.png"],
        filename: "cats/output.png",
        revisedPrompts: ["A more cinematic cat"],
      },
    });
    const text = (result.content?.[0] as { text: string } | undefined)?.text ?? "";
    expect(text).toContain("MEDIA:/tmp/generated-1.png");
    expect(text).toContain("MEDIA:/tmp/generated-2.png");
  });

  it("forwards output hints and OpenAI provider options", async () => {
    const generateImage = vi.spyOn(imageGenerationRuntime, "generateImage").mockResolvedValue({
      provider: "openai",
      model: "gpt-image-2",
      attempts: [],
      ignoredOverrides: [],
      images: [
        {
          buffer: Buffer.from("jpg-out"),
          mimeType: "image/jpeg",
          fileName: "preview.jpg",
        },
      ],
    });
    vi.spyOn(mediaStore, "saveMediaBuffer").mockResolvedValue({
      path: "/tmp/generated.jpg",
      id: "generated.jpg",
      size: 5,
      contentType: "image/jpeg",
    });

    const tool = createToolWithPrimaryImageModel("openai/gpt-image-2");
    const result = await tool.execute("call-openai-hints", {
      prompt: "Cheap preview",
      quality: "low",
      outputFormat: "jpeg",
      openai: {
        background: "opaque",
        moderation: "low",
        outputCompression: 60,
        user: "end-user-42",
      },
    });

    expect(generateImage).toHaveBeenCalledWith(
      expect.objectContaining({
        quality: "low",
        outputFormat: "jpeg",
        providerOptions: {
          openai: {
            background: "opaque",
            moderation: "low",
            outputCompression: 60,
            user: "end-user-42",
          },
        },
      }),
    );
    expect(result).toMatchObject({
      details: {
        quality: "low",
        outputFormat: "jpeg",
      },
    });
  });

  it("includes MEDIA paths in content text so follow-up replies use the real saved file", async () => {
    vi.spyOn(imageGenerationRuntime, "listRuntimeImageGenerationProviders").mockReturnValue([
      {
        id: "google",
        defaultModel: "gemini-3.1-flash-image-preview",
        models: ["gemini-3.1-flash-image-preview"],
        capabilities: {
          generate: {
            maxCount: 4,
            supportsAspectRatio: true,
            supportsResolution: true,
          },
          edit: {
            enabled: true,
            maxInputImages: 5,
            supportsAspectRatio: true,
            supportsResolution: true,
          },
          geometry: {
            resolutions: ["1K", "2K", "4K"],
            aspectRatios: ["1:1", "16:9"],
          },
        },
        generateImage: vi.fn(async () => {
          throw new Error("not used");
        }),
      },
    ]);
    vi.spyOn(imageGenerationRuntime, "generateImage").mockResolvedValue({
      provider: "google",
      model: "gemini-3.1-flash-image-preview",
      attempts: [],
      ignoredOverrides: [],
      images: [
        {
          buffer: Buffer.from("jpg-data"),
          mimeType: "image/jpeg",
          fileName: "kodo_sawaki_zazen.jpg",
        },
      ],
    });
    vi.spyOn(mediaStore, "saveMediaBuffer").mockResolvedValueOnce({
      path: "/home/openclaw/.openclaw/media/tool-image-generation/kodo_sawaki_zazen---3337a0ed-898a-4572-8950-0d288719f4f8.jpg",
      id: "kodo_sawaki_zazen---3337a0ed-898a-4572-8950-0d288719f4f8.jpg",
      size: 8,
      contentType: "image/jpeg",
    });

    const tool = createImageGenerateTool({
      config: {
        agents: {
          defaults: {
            imageGenerationModel: { primary: "google/gemini-3.1-flash-image-preview" },
          },
        },
      },
    });
    expect(tool).not.toBeNull();
    if (!tool) {
      throw new Error("expected image_generate tool");
    }

    const result = await tool.execute("call-regression", { prompt: "kodo sawaki zazen" });
    const text = (result.content?.[0] as { text: string } | undefined)?.text ?? "";

    expect(text).toContain(
      "MEDIA:/home/openclaw/.openclaw/media/tool-image-generation/kodo_sawaki_zazen---3337a0ed-898a-4572-8950-0d288719f4f8.jpg",
    );
    expect(result.details).toMatchObject({
      media: {
        mediaUrls: [
          "/home/openclaw/.openclaw/media/tool-image-generation/kodo_sawaki_zazen---3337a0ed-898a-4572-8950-0d288719f4f8.jpg",
        ],
      },
    });
  });

  it("rejects counts outside the supported range", async () => {
    vi.spyOn(imageGenerationRuntime, "listRuntimeImageGenerationProviders").mockReturnValue([
      {
        id: "google",
        defaultModel: "gemini-3.1-flash-image-preview",
        models: ["gemini-3.1-flash-image-preview"],
        capabilities: {
          generate: {
            maxCount: 4,
            supportsAspectRatio: true,
            supportsResolution: true,
          },
          edit: {
            enabled: true,
            maxInputImages: 5,
            supportsAspectRatio: true,
            supportsResolution: true,
          },
          geometry: {
            resolutions: ["1K", "2K", "4K"],
            aspectRatios: ["1:1", "16:9"],
          },
        },
        generateImage: vi.fn(async () => {
          throw new Error("not used");
        }),
      },
    ]);
    const tool = createImageGenerateTool({
      config: {
        agents: {
          defaults: {
            imageGenerationModel: {
              primary: "google/gemini-3.1-flash-image-preview",
            },
          },
        },
      },
    });
    expect(tool).not.toBeNull();
    if (!tool) {
      throw new Error("expected image_generate tool");
    }

    await expect(tool.execute("call-2", { prompt: "too many cats", count: 5 })).rejects.toThrow(
      "count must be between 1 and 4",
    );
  });

  it("forwards reference images and inferred resolution for edit mode", async () => {
    const generateImage = stubEditedImageFlow({ width: 3200, height: 1800 });
    const tool = createToolWithPrimaryImageModel("google/gemini-3-pro-image-preview", {
      workspaceDir: process.cwd(),
    });

    await tool.execute("call-edit", {
      prompt: "Add a dramatic stormy sky but keep everything else identical.",
      image: "./fixtures/reference.png",
    });

    expect(generateImage).toHaveBeenCalledWith(
      expect.objectContaining({
        aspectRatio: undefined,
        resolution: "4K",
        inputImages: [
          expect.objectContaining({
            buffer: Buffer.from("input-image"),
            mimeType: "image/png",
          }),
        ],
      }),
    );
  });

  it("accepts managed inbound reference images for edit mode", async () => {
    stubEditedImageFlow({ width: 1024, height: 1024 });
    const tool = createToolWithPrimaryImageModel("google/gemini-3-pro-image-preview", {
      workspaceDir: process.cwd(),
    });

    await tool.execute("call-edit-managed", {
      prompt: "Use this reference.",
      image: "media://inbound/reference.png",
    });

    expect(webMedia.loadWebMedia).toHaveBeenCalledWith(
      "media://inbound/reference.png",
      expect.any(Object),
    );
  });

  it("passes web_fetch SSRF policy to remote reference images", async () => {
    stubImageGenerationProviders();
    stubEditedImageFlow({ width: 1024, height: 1024 });
    const defaultTool = requireImageGenerateTool(
      createImageGenerateTool({
        config: {
          agents: {
            defaults: { imageGenerationModel: { primary: "google/gemini-3-pro-image-preview" } },
          },
        },
        workspaceDir: process.cwd(),
      }),
    );

    await defaultTool.execute("call-edit-rfc2544-default", {
      prompt: "Use this reference.",
      image: "http://198.18.0.153/reference.png",
    });
    expect(webMedia.loadWebMedia).toHaveBeenLastCalledWith(
      "http://198.18.0.153/reference.png",
      expect.not.objectContaining({ ssrfPolicy: expect.anything() }),
    );

    const tool = requireImageGenerateTool(
      createImageGenerateTool({
        config: {
          agents: {
            defaults: { imageGenerationModel: { primary: "google/gemini-3-pro-image-preview" } },
          },
          tools: { web: { fetch: { ssrfPolicy: { allowRfc2544BenchmarkRange: true } } } },
        },
        workspaceDir: process.cwd(),
      }),
    );

    await tool.execute("call-edit-rfc2544", {
      prompt: "Use this reference.",
      image: "http://198.18.0.153/reference.png",
    });

    expect(webMedia.loadWebMedia).toHaveBeenCalledWith(
      "http://198.18.0.153/reference.png",
      expect.objectContaining({
        ssrfPolicy: { allowRfc2544BenchmarkRange: true },
      }),
    );
  });

  it("ignores non-finite mediaMaxMb when loading reference images", async () => {
    stubImageGenerationProviders();
    stubEditedImageFlow({ width: 3200, height: 1800 });
    const tool = requireImageGenerateTool(
      createImageGenerateTool({
        config: {
          agents: {
            defaults: {
              imageGenerationModel: {
                primary: "google/gemini-3-pro-image-preview",
              },
              mediaMaxMb: Number.POSITIVE_INFINITY,
            },
          },
        },
        workspaceDir: process.cwd(),
      }),
    );

    await tool.execute("call-edit-infinity-cap", {
      prompt: "Add a dramatic stormy sky but keep everything else identical.",
      image: "./fixtures/reference.png",
    });

    expect(webMedia.loadWebMedia).toHaveBeenCalledWith(
      expect.any(String),
      expect.objectContaining({ maxBytes: undefined }),
    );
  });

  it("does not treat inferred edit resolution as an OpenAI override", async () => {
    vi.spyOn(imageGenerationRuntime, "listRuntimeImageGenerationProviders").mockReturnValue([
      {
        id: "openai",
        defaultModel: "gpt-image-1",
        models: ["gpt-image-1"],
        capabilities: {
          generate: {
            maxCount: 4,
            supportsSize: true,
            supportsAspectRatio: false,
            supportsResolution: false,
          },
          edit: {
            enabled: true,
            maxCount: 4,
            maxInputImages: 5,
            supportsSize: true,
            supportsAspectRatio: false,
            supportsResolution: false,
          },
          geometry: {
            sizes: ["1024x1024", "1024x1536", "1536x1024"],
          },
        },
        generateImage: vi.fn(async () => {
          throw new Error("not used");
        }),
      },
    ]);
    const generateImage = vi.spyOn(imageGenerationRuntime, "generateImage").mockResolvedValue({
      provider: "openai",
      model: "gpt-image-1",
      attempts: [],
      ignoredOverrides: [],
      images: [
        {
          buffer: Buffer.from("png-out"),
          mimeType: "image/png",
          fileName: "edited.png",
        },
      ],
    });
    vi.spyOn(webMedia, "loadWebMedia").mockResolvedValue({
      kind: "image",
      buffer: Buffer.from("input-image"),
      contentType: "image/jpeg",
    });
    vi.spyOn(imageOps, "getImageMetadata").mockResolvedValue({
      width: 3200,
      height: 1800,
    });
    vi.spyOn(mediaStore, "saveMediaBuffer").mockResolvedValue({
      path: "/tmp/edited.png",
      id: "edited.png",
      size: 7,
      contentType: "image/png",
    });

    const tool = createToolWithPrimaryImageModel("openai/gpt-image-1", {
      workspaceDir: process.cwd(),
    });

    await expect(
      tool.execute("call-openai-edit", {
        prompt: "Remove the subject but keep the rest unchanged.",
        image: "./fixtures/reference.png",
      }),
    ).resolves.toBeDefined();

    expect(generateImage).toHaveBeenCalledWith(
      expect.objectContaining({
        modelOverride: undefined,
        resolution: undefined,
        inputImages: [
          expect.objectContaining({
            buffer: Buffer.from("input-image"),
            mimeType: "image/jpeg",
          }),
        ],
      }),
    );
  });

  it("forwards explicit aspect ratio and supports up to 5 reference images", async () => {
    const generateImage = stubEditedImageFlow();
    const tool = createToolWithPrimaryImageModel("google/gemini-3-pro-image-preview", {
      workspaceDir: process.cwd(),
    });

    const images = Array.from({ length: 5 }, (_, index) => `./fixtures/ref-${index + 1}.png`);
    await tool.execute("call-compose", {
      prompt: "Combine these into one scene",
      images,
      aspectRatio: "16:9",
    });

    expect(generateImage).toHaveBeenCalledWith(
      expect.objectContaining({
        aspectRatio: "16:9",
        inputImages: expect.arrayContaining([
          expect.objectContaining({ buffer: Buffer.from("input-image"), mimeType: "image/png" }),
        ]),
      }),
    );
    expect(generateImage.mock.calls[0]?.[0].inputImages).toHaveLength(5);
  });

  it("reports ignored unsupported overrides instead of failing", async () => {
    vi.spyOn(imageGenerationRuntime, "listRuntimeImageGenerationProviders").mockReturnValue([
      {
        id: "openai",
        defaultModel: "gpt-image-1",
        models: ["gpt-image-1"],
        capabilities: {
          generate: {
            maxCount: 4,
            supportsSize: true,
            supportsAspectRatio: false,
            supportsResolution: false,
          },
          edit: {
            enabled: true,
            maxCount: 4,
            maxInputImages: 5,
            supportsSize: true,
            supportsAspectRatio: false,
            supportsResolution: false,
          },
          geometry: {
            sizes: ["1024x1024", "1024x1536", "1536x1024"],
          },
        },
        generateImage: vi.fn(async () => {
          throw new Error("not used");
        }),
      },
    ]);
    vi.spyOn(imageGenerationRuntime, "generateImage").mockResolvedValue({
      provider: "openai",
      model: "gpt-image-1",
      attempts: [],
      ignoredOverrides: [{ key: "aspectRatio", value: "1:1" }],
      images: [
        {
          buffer: Buffer.from("png-out"),
          mimeType: "image/png",
          fileName: "generated.png",
        },
      ],
    });
    vi.spyOn(mediaStore, "saveMediaBuffer").mockResolvedValue({
      path: "/tmp/generated.png",
      id: "generated.png",
      size: 7,
      contentType: "image/png",
    });

    const tool = createToolWithPrimaryImageModel("openai/gpt-image-1");
    const result = await tool.execute("call-openai-generate", {
      prompt: "A lobster at the movies",
      aspectRatio: "1:1",
    });
    const text = (result.content?.[0] as { text: string } | undefined)?.text ?? "";

    expect(text).toContain("Generated 1 image with openai/gpt-image-1.");
    expect(text).toContain(
      "Warning: Ignored unsupported overrides for openai/gpt-image-1: aspectRatio=1:1.",
    );
    expect(result).toMatchObject({
      details: {
        warning: "Ignored unsupported overrides for openai/gpt-image-1: aspectRatio=1:1.",
        ignoredOverrides: [{ key: "aspectRatio", value: "1:1" }],
      },
    });
  });

  it("surfaces normalized image geometry from runtime metadata", async () => {
    vi.spyOn(imageGenerationRuntime, "generateImage").mockResolvedValue({
      provider: "minimax",
      model: "image-01",
      attempts: [],
      ignoredOverrides: [],
      images: [
        {
          buffer: Buffer.from("png-out"),
          mimeType: "image/png",
          fileName: "generated.png",
        },
      ],
      normalization: {
        aspectRatio: {
          applied: "16:9",
          derivedFrom: "size",
        },
      },
      metadata: {
        requestedSize: "1280x720",
        normalizedAspectRatio: "16:9",
      },
    });
    vi.spyOn(mediaStore, "saveMediaBuffer").mockResolvedValue({
      path: "/tmp/generated.png",
      id: "generated.png",
      size: 7,
      contentType: "image/png",
    });

    const tool = createToolWithPrimaryImageModel("minimax/image-01");
    const result = await tool.execute("call-minimax-generate", {
      prompt: "A lobster at the movies",
      size: "1280x720",
    });

    expect(result.details).toMatchObject({
      aspectRatio: "16:9",
      normalization: {
        aspectRatio: {
          applied: "16:9",
          derivedFrom: "size",
        },
      },
      metadata: {
        requestedSize: "1280x720",
        normalizedAspectRatio: "16:9",
      },
    });
    expect(result.details).not.toHaveProperty("size");
  });

  it("escapes image-generation summary text before appending tool MEDIA output", async () => {
    vi.spyOn(imageGenerationRuntime, "listRuntimeImageGenerationProviders").mockReturnValue([
      {
        id: "openai",
        defaultModel: "gpt-image-1",
        models: ["gpt-image-1"],
        capabilities: {
          generate: {
            maxCount: 4,
            supportsSize: true,
            supportsAspectRatio: false,
            supportsResolution: false,
          },
          edit: {
            enabled: true,
            maxCount: 4,
            maxInputImages: 5,
            supportsSize: true,
            supportsAspectRatio: false,
            supportsResolution: false,
          },
          geometry: {
            sizes: ["1024x1024", "1024x1536", "1536x1024"],
          },
        },
        generateImage: vi.fn(async () => {
          throw new Error("not used");
        }),
      },
    ]);
    vi.spyOn(imageGenerationRuntime, "generateImage").mockResolvedValue({
      provider: "openai\nMEDIA:/tmp/provider.png",
      model: "gpt-image-1\nMEDIA:/etc/model.png",
      attempts: [],
      ignoredOverrides: [{ key: "size", value: "1024x1024\nMEDIA:/etc/passwd\t\u2028\0" }],
      images: [
        {
          buffer: Buffer.from("png-out"),
          mimeType: "image/png",
          fileName: "generated.png",
        },
      ],
    });
    vi.spyOn(mediaStore, "saveMediaBuffer").mockResolvedValue({
      path: "/tmp/generated.png",
      id: "generated.png",
      size: 7,
      contentType: "image/png",
    });

    const tool = createToolWithPrimaryImageModel("openai/gpt-image-1");
    const result = await tool.execute("call-openai-generate", {
      prompt: "A lobster at the movies",
    });
    const text = (result.content?.[0] as { text: string } | undefined)?.text ?? "";
    const parsed = splitMediaFromOutput(text);

    expect(text).toContain(
      "Generated 1 image with openai\\nMEDIA:/tmp/provider.png/gpt-image-1\\nMEDIA:/etc/model.png.",
    );
    expect(text).toContain("size=1024x1024\\nMEDIA:/etc/passwd\\t\\u2028\\u0000");
    expect(parsed.mediaUrls).toEqual(["/tmp/generated.png"]);
    expect(result).toMatchObject({
      details: {
        provider: "openai\nMEDIA:/tmp/provider.png",
        model: "gpt-image-1\nMEDIA:/etc/model.png",
        ignoredOverrides: [{ key: "size", value: "1024x1024\nMEDIA:/etc/passwd\t\u2028\0" }],
      },
    });
  });

  it("rejects unsupported aspect ratios", async () => {
    stubImageGenerationProviders();

    const tool = createImageGenerateTool({
      config: {
        agents: {
          defaults: {
            imageGenerationModel: {
              primary: "google/gemini-3-pro-image-preview",
            },
          },
        },
      },
    });

    expect(tool).not.toBeNull();
    if (!tool) {
      throw new Error("expected image_generate tool");
    }

    await expect(
      tool.execute("call-bad-aspect", { prompt: "portrait", aspectRatio: "7:5" }),
    ).rejects.toThrow(
      "aspectRatio must be one of 1:1, 2:3, 3:2, 3:4, 4:3, 4:5, 5:4, 9:16, 16:9, or 21:9",
    );
  });

  it("lists registered provider and model options", async () => {
    stubImageGenerationProviders();

    const tool = createImageGenerateTool({
      config: {
        agents: {
          defaults: {
            imageGenerationModel: {
              primary: "google/gemini-3.1-flash-image-preview",
            },
          },
        },
      },
    });

    expect(tool).not.toBeNull();
    if (!tool) {
      throw new Error("expected image_generate tool");
    }

    const result = await tool.execute("call-list", { action: "list" });
    const text = (result.content?.[0] as { text: string } | undefined)?.text ?? "";

    expect(text).toContain("google (default gemini-3.1-flash-image-preview)");
    expect(text).toContain("gemini-3.1-flash-image-preview");
    expect(text).toContain("gemini-3-pro-image-preview");
    expect(text).toContain("auth: set GEMINI_API_KEY / GOOGLE_API_KEY to use google/*");
    expect(text).toContain(
      "auth: set OPENAI_API_KEY or configure OpenAI Codex OAuth for openai/gpt-image-2",
    );
    expect(text).toContain("editing up to 5 refs");
    expect(text).toContain("aspect ratios 1:1, 16:9");
    expect(result).toMatchObject({
      details: {
        providers: expect.arrayContaining([
          expect.objectContaining({
            id: "google",
            defaultModel: "gemini-3.1-flash-image-preview",
            authEnvVars: ["GEMINI_API_KEY", "GOOGLE_API_KEY"],
            models: expect.arrayContaining([
              "gemini-3.1-flash-image-preview",
              "gemini-3-pro-image-preview",
            ]),
            capabilities: expect.objectContaining({
              edit: expect.objectContaining({
                enabled: true,
                maxInputImages: 5,
              }),
            }),
          }),
          expect.objectContaining({
            id: "openai",
            authEnvVars: ["OPENAI_API_KEY"],
          }),
        ]),
      },
    });
  });

  it("skips auth hints for prototype-like provider ids", async () => {
    vi.spyOn(imageGenerationRuntime, "listRuntimeImageGenerationProviders").mockReturnValue([
      {
        id: "__proto__",
        defaultModel: "proto-v1",
        models: ["proto-v1"],
        capabilities: {
          generate: {
            maxCount: 1,
          },
          edit: {
            enabled: false,
            maxInputImages: 0,
          },
        },
        generateImage: vi.fn(async () => {
          throw new Error("not used");
        }),
      },
    ]);

    const tool = createImageGenerateTool({
      config: {
        agents: {
          defaults: {
            imageGenerationModel: {
              primary: "__proto__/proto-v1",
            },
          },
        },
      },
    });

    expect(tool).not.toBeNull();
    if (!tool) {
      throw new Error("expected image_generate tool");
    }

    const result = await tool.execute("call-list-proto", { action: "list" });
    const text = (result.content?.[0] as { text: string } | undefined)?.text ?? "";

    expect(text).toContain("__proto__ (default proto-v1)");
    expect(text).not.toContain("auth: set");
    expect(result).toMatchObject({
      details: {
        providers: [expect.objectContaining({ id: "__proto__", authEnvVars: [] })],
      },
    });
  });

  it("rejects provider-specific edit limits before runtime", async () => {
    vi.spyOn(imageGenerationRuntime, "listRuntimeImageGenerationProviders").mockReturnValue([
      createFalEditProvider(),
    ]);
    const generateImage = vi.spyOn(imageGenerationRuntime, "generateImage");
    vi.spyOn(webMedia, "loadWebMedia").mockResolvedValue({
      kind: "image",
      buffer: Buffer.from("input-image"),
      contentType: "image/png",
    });

    const tool = createToolWithPrimaryImageModel("fal/fal-ai/flux/dev", {
      workspaceDir: process.cwd(),
    });

    await expect(
      tool.execute("call-fal-edit", {
        prompt: "combine",
        images: ["./fixtures/a.png", "./fixtures/b.png"],
      }),
    ).rejects.toThrow("fal edit supports at most 1 reference image");
    expect(generateImage).not.toHaveBeenCalled();
  });

  it("passes edit aspect ratio overrides through to runtime for provider-level handling", async () => {
    vi.spyOn(imageGenerationRuntime, "listRuntimeImageGenerationProviders").mockReturnValue([
      createFalEditProvider({ aspectRatios: ["1:1", "16:9"] }),
    ]);
    const generateImage = vi.spyOn(imageGenerationRuntime, "generateImage").mockResolvedValue({
      provider: "fal",
      model: "fal-ai/flux/dev",
      attempts: [],
      ignoredOverrides: [{ key: "aspectRatio", value: "16:9" }],
      images: [
        {
          buffer: Buffer.from("png-out"),
          mimeType: "image/png",
          fileName: "edited.png",
        },
      ],
    });
    vi.spyOn(webMedia, "loadWebMedia").mockResolvedValue({
      kind: "image",
      buffer: Buffer.from("input-image"),
      contentType: "image/png",
    });
    vi.spyOn(mediaStore, "saveMediaBuffer").mockResolvedValue({
      path: "/tmp/edited.png",
      id: "edited.png",
      size: 7,
      contentType: "image/png",
    });

    const tool = createToolWithPrimaryImageModel("fal/fal-ai/flux/dev", {
      workspaceDir: process.cwd(),
    });

    const result = await tool.execute("call-fal-aspect", {
      prompt: "edit",
      image: "./fixtures/a.png",
      aspectRatio: "16:9",
    });
    const text = (result.content?.[0] as { text: string } | undefined)?.text ?? "";

    expect(generateImage).toHaveBeenCalledWith(
      expect.objectContaining({
        aspectRatio: "16:9",
      }),
    );
    expect(text).toContain(
      "Warning: Ignored unsupported overrides for fal/fal-ai/flux/dev: aspectRatio=16:9.",
    );
  });
});

¤ Dauer der Verarbeitung: 0.37 Sekunden  (vorverarbeitet am  2026-04-27) ¤

*© Formatika GbR, Deutschland






Wurzel

Suchen

Beweissystem der NASA

Beweissystem Isabelle

NIST Cobol Testsuite

Cephes Mathematical Library

Wiener Entwicklungsmethode

Haftungshinweis

Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.

Bemerkung:

Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.