Files
openclaw/extensions/deepinfra/image-generation-provider.test.ts
NIO 527f8f0cbb fix(image-gen): bound image generation provider JSON response reads (#96495)
* fix(image-gen): bound image generation provider JSON response reads

Route success JSON reads through readProviderJsonResponse (16 MiB cap)
in openrouter, google, fal, minimax, openai, and vydra image generation
providers to prevent OOM from oversized or hostile endpoint responses.
Mirrors the response-limit campaign already applied to other provider paths.

AI-assisted.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(image-gen): size bounded JSON caps for inline image payloads

Signed-off-by: sallyom <somalley@redhat.com>

---------

Signed-off-by: sallyom <somalley@redhat.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: sallyom <somalley@redhat.com>
2026-06-26 07:08:30 -04:00

235 lines
8.2 KiB
TypeScript

// Deepinfra tests cover image generation provider plugin behavior.
import { afterAll, afterEach, describe, expect, it, vi } from "vitest";
import { buildDeepInfraImageGenerationProvider } from "./image-generation-provider.js";
const {
assertOkOrThrowHttpErrorMock,
postJsonRequestMock,
postMultipartRequestMock,
resolveApiKeyForProviderMock,
resolveProviderHttpRequestConfigMock,
createProviderOperationDeadlineMock,
resolveProviderOperationTimeoutMsMock,
} = vi.hoisted(() => ({
assertOkOrThrowHttpErrorMock: vi.fn(async () => {}),
postJsonRequestMock: vi.fn(),
postMultipartRequestMock: vi.fn(),
resolveApiKeyForProviderMock: vi.fn(async () => ({ apiKey: "deepinfra-key" })),
createProviderOperationDeadlineMock: vi.fn((params: Record<string, unknown>) => params),
resolveProviderOperationTimeoutMsMock: vi.fn(
(params: Record<string, unknown>) => params.defaultTimeoutMs,
),
resolveProviderHttpRequestConfigMock: vi.fn((params: Record<string, unknown>) => ({
baseUrl: params.baseUrl ?? params.defaultBaseUrl ?? "https://api.deepinfra.com/v1/openai",
allowPrivateNetwork: false,
headers: new Headers(params.defaultHeaders as HeadersInit | undefined),
dispatcherPolicy: undefined,
})),
}));
vi.mock("openclaw/plugin-sdk/provider-auth-runtime", () => ({
resolveApiKeyForProvider: resolveApiKeyForProviderMock,
}));
vi.mock("openclaw/plugin-sdk/provider-http", async () => {
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/provider-http")>(
"openclaw/plugin-sdk/provider-http",
);
return {
assertOkOrThrowHttpError: assertOkOrThrowHttpErrorMock,
createProviderOperationDeadline: createProviderOperationDeadlineMock,
postJsonRequest: postJsonRequestMock,
postMultipartRequest: postMultipartRequestMock,
readProviderJsonResponse: actual.readProviderJsonResponse,
resolveProviderHttpRequestConfig: resolveProviderHttpRequestConfigMock,
resolveProviderOperationTimeoutMs: resolveProviderOperationTimeoutMsMock,
sanitizeConfiguredModelProviderRequest: vi.fn((request) => request),
};
});
afterAll(() => {
vi.doUnmock("openclaw/plugin-sdk/provider-auth-runtime");
vi.doUnmock("openclaw/plugin-sdk/provider-http");
vi.resetModules();
});
function requireFirstMockArg(mock: ReturnType<typeof vi.fn>, label: string): unknown {
const [call] = mock.mock.calls;
if (!call) {
throw new Error(`expected ${label}`);
}
return call[0];
}
function requireFirstMockObjectArg(mock: ReturnType<typeof vi.fn>, label: string): object {
const value = requireFirstMockArg(mock, label);
if (!value || typeof value !== "object" || Array.isArray(value)) {
throw new Error(`expected ${label}`);
}
return value;
}
function jsonResponse(payload: unknown): Response {
return new Response(JSON.stringify(payload), {
status: 200,
headers: { "Content-Type": "application/json" },
});
}
describe("deepinfra image generation provider", () => {
afterEach(() => {
assertOkOrThrowHttpErrorMock.mockClear();
postJsonRequestMock.mockReset();
postMultipartRequestMock.mockReset();
resolveApiKeyForProviderMock.mockClear();
resolveProviderHttpRequestConfigMock.mockClear();
});
it("declares generation and single-reference edit support", () => {
const provider = buildDeepInfraImageGenerationProvider();
expect(provider.id).toBe("deepinfra");
expect(provider.defaultModel).toBe("black-forest-labs/FLUX-1-schnell");
expect(provider.capabilities.generate.maxCount).toBe(4);
expect(provider.capabilities.edit.enabled).toBe(true);
expect(provider.capabilities.edit.maxInputImages).toBe(1);
});
it("sends OpenAI-compatible image generation requests and sniffs JPEG output", async () => {
const release = vi.fn(async () => {});
const jpegBytes = Buffer.from([0xff, 0xd8, 0xff, 0x00]);
postJsonRequestMock.mockResolvedValue({
response: jsonResponse({
data: [{ b64_json: jpegBytes.toString("base64"), revised_prompt: "red square" }],
}),
release,
});
const provider = buildDeepInfraImageGenerationProvider();
const result = await provider.generateImage({
provider: "deepinfra",
model: "deepinfra/black-forest-labs/FLUX-1-schnell",
prompt: "red square",
count: 2,
size: "512x512",
timeoutMs: 12_345,
cfg: {
models: {
providers: {
deepinfra: {
baseUrl: "https://api.deepinfra.com/v1/openai/",
},
},
},
} as never,
});
expect(resolveProviderHttpRequestConfigMock.mock.calls).toEqual([
[
{
baseUrl: "https://api.deepinfra.com/v1/openai",
defaultBaseUrl: "https://api.deepinfra.com/v1/openai",
allowPrivateNetwork: false,
request: undefined,
defaultHeaders: {
Authorization: "Bearer deepinfra-key",
},
provider: "deepinfra",
capability: "image",
transport: "http",
},
],
]);
expect(postJsonRequestMock).toHaveBeenCalledOnce();
const jsonRequest = requireFirstMockArg(postJsonRequestMock, "DeepInfra JSON image request");
const jsonRequestHeaders = Reflect.get(jsonRequest ?? {}, "headers");
expect(jsonRequestHeaders).toBeInstanceOf(Headers);
expect(Object.fromEntries((jsonRequestHeaders as Headers).entries())).toEqual({
authorization: "Bearer deepinfra-key",
"content-type": "application/json",
});
expect(jsonRequest).toEqual({
url: "https://api.deepinfra.com/v1/openai/images/generations",
headers: jsonRequestHeaders,
timeoutMs: 12_345,
body: {
model: "black-forest-labs/FLUX-1-schnell",
prompt: "red square",
n: 2,
size: "512x512",
response_format: "b64_json",
},
fetchFn: fetch,
allowPrivateNetwork: false,
dispatcherPolicy: undefined,
});
expect(result.images).toHaveLength(1);
const [firstImage] = result.images;
if (!firstImage) {
throw new Error("Expected generated DeepInfra image");
}
expect(firstImage).toEqual({
buffer: jpegBytes,
mimeType: "image/jpeg",
fileName: "image-1.jpg",
revisedPrompt: "red square",
});
expect(release).toHaveBeenCalledOnce();
});
it("sends image edits as multipart OpenAI-compatible requests", async () => {
postMultipartRequestMock.mockResolvedValue({
response: jsonResponse({
data: [
{
b64_json: Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]).toString(
"base64",
),
},
],
}),
release: vi.fn(async () => {}),
});
const provider = buildDeepInfraImageGenerationProvider();
const result = await provider.generateImage({
provider: "deepinfra",
model: "black-forest-labs/FLUX-1-schnell",
prompt: "make it neon",
inputImages: [{ buffer: Buffer.from("source"), mimeType: "image/png" }],
cfg: {} as never,
});
expect(postMultipartRequestMock).toHaveBeenCalledOnce();
const multipartRequest = requireFirstMockObjectArg(
postMultipartRequestMock,
"DeepInfra multipart image request",
);
const multipartHeaders = Reflect.get(multipartRequest, "headers");
expect(multipartHeaders).toBeInstanceOf(Headers);
expect(Object.fromEntries((multipartHeaders as Headers).entries())).toEqual({
authorization: "Bearer deepinfra-key",
});
const form = Reflect.get(multipartRequest, "body") as FormData;
expect(multipartRequest).toEqual({
url: "https://api.deepinfra.com/v1/openai/images/edits",
headers: multipartHeaders,
body: form,
timeoutMs: undefined,
fetchFn: fetch,
allowPrivateNetwork: false,
dispatcherPolicy: undefined,
});
expect(form.get("model")).toBe("black-forest-labs/FLUX-1-schnell");
expect(form.get("prompt")).toBe("make it neon");
expect(form.get("response_format")).toBe("b64_json");
expect(form.get("image")).toBeInstanceOf(File);
expect(result.images).toHaveLength(1);
const [image] = result.images;
if (!image) {
throw new Error("Expected edited DeepInfra image");
}
expect(image.mimeType).toBe("image/png");
});
});