import { beforeEach, describe, expect, it, vi } from "vitest" ;
import {
getMediaGenerationRuntimeMocks,
resetImageGenerationRuntimeMocks,
} from "../../test/helpers/media-generation/runtime-module-mocks.js" ;
import type { OpenClawConfig } from "../config/config.js" ;
import { generateImage, listRuntimeImageGenerationProviders } from "./runtime.js" ;
import type { ImageGenerationProvider } from "./types.js" ;
const mocks = getMediaGenerationRuntimeMocks();
vi.mock("./model-ref.js" , () => ({
parseImageGenerationModelRef: mocks.parseImageGenerationModelRef,
}));
vi.mock("./provider-registry.js" , () => ({
getImageGenerationProvider: mocks.getImageGenerationProvider,
listImageGenerationProviders: mocks.listImageGenerationProviders,
}));
describe("image-generation runtime" , () => {
beforeEach(() => {
resetImageGenerationRuntimeMocks();
});
it("generates images through the active image-generation provider" , async () => {
const authStore = { version: 1 , profiles: {} } as const ;
let seenAuthStore: unknown;
let seenTimeoutMs: number | undefined;
mocks.resolveAgentModelPrimaryValue.mockReturnValue("image-plugin/img-v1" );
const provider: ImageGenerationProvider = {
id: "image-plugin" ,
capabilities: {
generate: {},
edit: { enabled: false },
},
async generateImage(req: { authStore?: unknown; timeoutMs?: number }) {
seenAuthStore = req.authStore;
seenTimeoutMs = req.timeoutMs;
return {
images: [
{
buffer: Buffer.from("png-bytes" ),
mimeType: "image/png" ,
fileName: "sample.png" ,
},
],
model: "img-v1" ,
};
},
};
mocks.getImageGenerationProvider.mockReturnValue(provider);
const result = await generateImage({
cfg: {
agents: {
defaults: {
imageGenerationModel: { primary: "image-plugin/img-v1" },
},
},
} as OpenClawConfig,
prompt: "draw a cat" ,
agentDir: "/tmp/agent" ,
authStore,
timeoutMs: 12 _345 ,
});
expect(result.provider).toBe("image-plugin" );
expect(result.model).toBe("img-v1" );
expect(result.attempts).toEqual([]);
expect(seenAuthStore).toEqual(authStore);
expect(seenTimeoutMs).toBe(12 _345 );
expect(result.images).toEqual([
{
buffer: Buffer.from("png-bytes" ),
mimeType: "image/png" ,
fileName: "sample.png" ,
},
]);
expect(result.ignoredOverrides).toEqual([]);
});
it("auto-detects and falls through to another configured image-generation provider by default" , async () => {
mocks.getImageGenerationProvider.mockImplementation((providerId: string) => {
if (providerId === "openai" ) {
return {
id: "openai" ,
defaultModel: "gpt-image-1" ,
capabilities: {
generate: {},
edit: { enabled: true },
},
isConfigured: () => true ,
async generateImage() {
throw new Error("OpenAI API key missing" );
},
};
}
if (providerId === "google" ) {
return {
id: "google" ,
defaultModel: "gemini-3.1-flash-image-preview" ,
capabilities: {
generate: {},
edit: { enabled: true },
},
isConfigured: () => true ,
async generateImage() {
return {
images: [{ buffer: Buffer.from("png-bytes" ), mimeType: "image/png" }],
model: "gemini-3.1-flash-image-preview" ,
};
},
};
}
return undefined;
});
mocks.listImageGenerationProviders.mockReturnValue([
{
id: "openai" ,
defaultModel: "gpt-image-1" ,
capabilities: {
generate: {},
edit: { enabled: true },
},
isConfigured: () => true ,
generateImage: async () => ({ images: [] }),
},
{
id: "google" ,
defaultModel: "gemini-3.1-flash-image-preview" ,
capabilities: {
generate: {},
edit: { enabled: true },
},
isConfigured: () => true ,
generateImage: async () => ({ images: [] }),
},
]);
const result = await generateImage({
cfg: {} as OpenClawConfig,
prompt: "draw a cat" ,
});
expect(result.provider).toBe("google" );
expect(result.model).toBe("gemini-3.1-flash-image-preview" );
expect(result.attempts).toEqual([
{
provider: "openai" ,
model: "gpt-image-1" ,
error: "OpenAI API key missing" ,
},
]);
expect(mocks.warn).toHaveBeenCalledWith(
"image-generation candidate failed: openai/gpt-image-1: OpenAI API key missing" ,
);
});
it("drops unsupported provider geometry overrides and reports them" , async () => {
let seenRequest:
| {
size?: string;
aspectRatio?: string;
resolution?: string;
}
| undefined;
mocks.resolveAgentModelPrimaryValue.mockReturnValue("openai/gpt-image-1" );
mocks.getImageGenerationProvider.mockReturnValue({
id: "openai" ,
capabilities: {
generate: {
supportsSize: true ,
supportsAspectRatio: false ,
supportsResolution: false ,
},
edit: {
enabled: true ,
supportsSize: true ,
supportsAspectRatio: false ,
supportsResolution: false ,
},
geometry: {
sizes: ["1024x1024" , "1024x1536" , "1536x1024" ],
},
},
async generateImage(req) {
seenRequest = {
size: req.size,
aspectRatio: req.aspectRatio,
resolution: req.resolution,
};
return {
images: [{ buffer: Buffer.from("png-bytes" ), mimeType: "image/png" }],
};
},
});
const result = await generateImage({
cfg: {
agents: {
defaults: {
imageGenerationModel: { primary: "openai/gpt-image-1" },
},
},
} as OpenClawConfig,
prompt: "draw a cat" ,
size: "1024x1024" ,
aspectRatio: "1:1" ,
resolution: "2K" ,
});
expect(seenRequest).toEqual({
size: "1024x1024" ,
aspectRatio: undefined,
resolution: undefined,
});
expect(result.ignoredOverrides).toEqual([
{ key: "aspectRatio" , value: "1:1" },
{ key: "resolution" , value: "2K" },
]);
});
it("filters image output hints by provider capabilities" , async () => {
let seenRequest:
| {
quality?: string;
outputFormat?: string;
providerOptions?: unknown;
}
| undefined;
mocks.resolveAgentModelPrimaryValue.mockReturnValue("openai/gpt-image-2" );
mocks.getImageGenerationProvider.mockReturnValue({
id: "openai" ,
capabilities: {
generate: {
supportsSize: true ,
},
edit: {
enabled: true ,
supportsSize: true ,
},
output: {
qualities: ["low" , "medium" , "high" , "auto" ],
formats: ["png" , "jpeg" , "webp" ],
},
},
async generateImage(req) {
seenRequest = {
quality: req.quality,
outputFormat: req.outputFormat,
providerOptions: req.providerOptions,
};
return {
images: [{ buffer: Buffer.from("jpeg-bytes" ), mimeType: "image/jpeg" }],
};
},
});
const result = await generateImage({
cfg: {
agents: {
defaults: {
imageGenerationModel: { primary: "openai/gpt-image-2" },
},
},
} as OpenClawConfig,
prompt: "draw a cheap preview" ,
quality: "low" ,
outputFormat: "jpeg" ,
providerOptions: {
openai: {
background: "opaque" ,
moderation: "low" ,
outputCompression: 60 ,
user: "end-user-42" ,
},
},
});
expect(seenRequest).toEqual({
quality: "low" ,
outputFormat: "jpeg" ,
providerOptions: {
openai: {
background: "opaque" ,
moderation: "low" ,
outputCompression: 60 ,
user: "end-user-42" ,
},
},
});
expect(result.ignoredOverrides).toEqual([]);
});
it("drops unsupported image output hints and reports them" , async () => {
let seenRequest:
| {
quality?: string;
outputFormat?: string;
}
| undefined;
mocks.resolveAgentModelPrimaryValue.mockReturnValue("vydra/grok-imagine" );
mocks.getImageGenerationProvider.mockReturnValue({
id: "vydra" ,
capabilities: {
generate: {},
edit: {
enabled: false ,
},
},
async generateImage(req) {
seenRequest = {
quality: req.quality,
outputFormat: req.outputFormat,
};
return {
images: [{ buffer: Buffer.from("png-bytes" ), mimeType: "image/png" }],
};
},
});
const result = await generateImage({
cfg: {
agents: {
defaults: {
imageGenerationModel: { primary: "vydra/grok-imagine" },
},
},
} as OpenClawConfig,
prompt: "draw a cat" ,
quality: "low" ,
outputFormat: "jpeg" ,
});
expect(seenRequest).toEqual({
quality: undefined,
outputFormat: undefined,
});
expect(result.ignoredOverrides).toEqual([
{ key: "quality" , value: "low" },
{ key: "outputFormat" , value: "jpeg" },
]);
});
it("maps requested size to the closest supported fallback geometry" , async () => {
let seenRequest:
| {
size?: string;
aspectRatio?: string;
resolution?: string;
}
| undefined;
mocks.resolveAgentModelPrimaryValue.mockReturnValue("minimax/image-01" );
mocks.getImageGenerationProvider.mockReturnValue({
id: "minimax" ,
capabilities: {
generate: {
supportsSize: false ,
supportsAspectRatio: true ,
supportsResolution: false ,
},
edit: {
enabled: true ,
supportsSize: false ,
supportsAspectRatio: true ,
supportsResolution: false ,
},
geometry: {
aspectRatios: ["1:1" , "16:9" ],
},
},
async generateImage(req) {
seenRequest = {
size: req.size,
aspectRatio: req.aspectRatio,
resolution: req.resolution,
};
return {
images: [{ buffer: Buffer.from("png-bytes" ), mimeType: "image/png" }],
model: "image-01" ,
};
},
});
const result = await generateImage({
cfg: {
agents: {
defaults: {
imageGenerationModel: { primary: "minimax/image-01" },
},
},
} as OpenClawConfig,
prompt: "draw a cat" ,
size: "1280x720" ,
});
expect(seenRequest).toEqual({
size: undefined,
aspectRatio: "16:9" ,
resolution: undefined,
});
expect(result.ignoredOverrides).toEqual([]);
expect(result.normalization).toMatchObject({
aspectRatio: {
applied: "16:9" ,
derivedFrom: "size" ,
},
});
expect(result.metadata).toMatchObject({
requestedSize: "1280x720" ,
normalizedAspectRatio: "16:9" ,
aspectRatioDerivedFromSize: "16:9" ,
});
});
it("lists runtime image-generation providers through the provider registry" , () => {
const providers: ImageGenerationProvider[] = [
{
id: "image-plugin" ,
defaultModel: "img-v1" ,
models: ["img-v1" , "img-v2" ],
capabilities: {
generate: {
supportsResolution: true ,
},
edit: {
enabled: true ,
maxInputImages: 3 ,
},
geometry: {
resolutions: ["1K" , "2K" ],
},
},
generateImage: async () => ({
images: [{ buffer: Buffer.from("png-bytes" ), mimeType: "image/png" }],
}),
},
];
mocks.listImageGenerationProviders.mockReturnValue(providers);
expect(listRuntimeImageGenerationProviders({ config: {} as OpenClawConfig })).toEqual(
providers,
);
expect(mocks.listImageGenerationProviders).toHaveBeenCalledWith({} as OpenClawConfig);
});
it("builds a generic config hint without hardcoded provider ids" , async () => {
mocks.listImageGenerationProviders.mockReturnValue([
{
id: "vision-one" ,
defaultModel: "paint-v1" ,
isConfigured: () => false ,
capabilities: {
generate: {},
edit: { enabled: false },
},
generateImage: async () => ({
images: [{ buffer: Buffer.from("png-bytes" ), mimeType: "image/png" }],
}),
},
{
id: "vision-two" ,
defaultModel: "paint-v2" ,
isConfigured: () => false ,
capabilities: {
generate: {},
edit: { enabled: false },
},
generateImage: async () => ({
images: [{ buffer: Buffer.from("png-bytes" ), mimeType: "image/png" }],
}),
},
]);
mocks.getProviderEnvVars.mockImplementation((providerId: string) => {
if (providerId === "vision-one" ) {
return ["VISION_ONE_API_KEY" ];
}
if (providerId === "vision-two" ) {
return ["VISION_TWO_API_KEY" ];
}
return [];
});
await expect(
generateImage({ cfg: {} as OpenClawConfig, prompt: "draw a cat" }),
).rejects.toThrow(
'No image-generation model configured. Set agents.defaults.imageGenerationModel.primary to a provider/model like "vision-one/paint-v1". If you want a specific provider, also configure that provider\' s auth/API key first (vision-one: VISION_ONE_API_KEY; vision-two: VISION_TWO_API_KEY).',
);
});
});
Messung V0.5 in Prozent C=100 H=98 G=98
¤ Dauer der Verarbeitung: 0.11 Sekunden
(vorverarbeitet am 2026-06-10)
¤
*© Formatika GbR, Deutschland