


























@@ -0,0 +1,201 @@
1+import { afterEach, describe, expect, it, vi } from "vitest";
2+import {
3+buildOpenRouterImageGenerationProvider,
4+extractOpenRouterImagesFromResponse,
5+} from "./image-generation-provider.js";
6+7+const {
8+ assertOkOrThrowHttpErrorMock,
9+ postJsonRequestMock,
10+ resolveApiKeyForProviderMock,
11+ resolveProviderHttpRequestConfigMock,
12+} = vi.hoisted(() => ({
13+assertOkOrThrowHttpErrorMock: vi.fn(async () => {}),
14+postJsonRequestMock: vi.fn(),
15+resolveApiKeyForProviderMock: vi.fn(async () => ({ apiKey: "openrouter-key" })),
16+resolveProviderHttpRequestConfigMock: vi.fn((params: Record<string, unknown>) => ({
17+baseUrl: params.baseUrl ?? params.defaultBaseUrl ?? "https://openrouter.ai/api/v1",
18+allowPrivateNetwork: false,
19+headers: new Headers(params.defaultHeaders as HeadersInit | undefined),
20+dispatcherPolicy: undefined,
21+})),
22+}));
23+24+vi.mock("openclaw/plugin-sdk/provider-auth-runtime", () => ({
25+resolveApiKeyForProvider: resolveApiKeyForProviderMock,
26+}));
27+28+vi.mock("openclaw/plugin-sdk/provider-http", () => ({
29+assertOkOrThrowHttpError: assertOkOrThrowHttpErrorMock,
30+postJsonRequest: postJsonRequestMock,
31+resolveProviderHttpRequestConfig: resolveProviderHttpRequestConfigMock,
32+}));
33+34+describe("openrouter image generation provider", () => {
35+afterEach(() => {
36+assertOkOrThrowHttpErrorMock.mockClear();
37+postJsonRequestMock.mockReset();
38+resolveApiKeyForProviderMock.mockClear();
39+resolveProviderHttpRequestConfigMock.mockClear();
40+});
41+42+it("builds provider metadata and capabilities", () => {
43+const provider = buildOpenRouterImageGenerationProvider();
44+expect(provider.id).toBe("openrouter");
45+expect(provider.label).toBe("OpenRouter");
46+expect(provider.defaultModel).toBe("google/gemini-3.1-flash-image-preview");
47+expect(provider.models).toContain("google/gemini-3-pro-image-preview");
48+expect(provider.capabilities.generate.maxCount).toBe(4);
49+expect(provider.capabilities.generate.supportsAspectRatio).toBe(true);
50+expect(provider.capabilities.edit.enabled).toBe(true);
51+expect(provider.capabilities.edit.maxInputImages).toBe(5);
52+});
53+54+it("sends chat completion image requests with Gemini image config and count", async () => {
55+const release = vi.fn(async () => {});
56+postJsonRequestMock.mockResolvedValue({
57+response: {
58+json: async () => ({
59+choices: [
60+{
61+message: {
62+images: [
63+{
64+imageUrl: {
65+url: `data:image/png;base64,${Buffer.from("png-one").toString("base64")}`,
66+},
67+},
68+],
69+},
70+},
71+],
72+}),
73+},
74+ release,
75+});
76+77+const provider = buildOpenRouterImageGenerationProvider();
78+const result = await provider.generateImage({
79+provider: "openrouter",
80+model: "google/gemini-3.1-flash-image-preview",
81+prompt: "draw a sticker",
82+aspectRatio: "16:9",
83+resolution: "2K",
84+count: 2,
85+timeoutMs: 12_345,
86+cfg: {
87+models: {
88+providers: {
89+openrouter: {
90+baseUrl: "https://custom.openrouter.test/api/v1",
91+},
92+},
93+},
94+} as never,
95+});
96+97+expect(resolveApiKeyForProviderMock).toHaveBeenCalledWith(
98+expect.objectContaining({ provider: "openrouter" }),
99+);
100+expect(resolveProviderHttpRequestConfigMock).toHaveBeenCalledWith(
101+expect.objectContaining({
102+provider: "openrouter",
103+capability: "image",
104+baseUrl: "https://custom.openrouter.test/api/v1",
105+}),
106+);
107+expect(postJsonRequestMock).toHaveBeenCalledWith(
108+expect.objectContaining({
109+url: "https://custom.openrouter.test/api/v1/chat/completions",
110+timeoutMs: 12_345,
111+body: expect.objectContaining({
112+model: "google/gemini-3.1-flash-image-preview",
113+modalities: ["image", "text"],
114+n: 2,
115+image_config: {
116+aspect_ratio: "16:9",
117+image_size: "2K",
118+},
119+messages: [
120+{
121+role: "user",
122+content: "draw a sticker",
123+},
124+],
125+}),
126+}),
127+);
128+expect(result.images[0]?.buffer.toString()).toBe("png-one");
129+expect(result.images[0]?.mimeType).toBe("image/png");
130+expect(release).toHaveBeenCalledOnce();
131+});
132+133+it("sends reference images as data URLs for edit-style requests", async () => {
134+postJsonRequestMock.mockResolvedValue({
135+response: {
136+json: async () => ({
137+choices: [
138+{
139+message: {
140+content: [
141+{
142+type: "image_url",
143+image_url: {
144+url: `data:image/webp;base64,${Buffer.from("webp-one").toString("base64")}`,
145+},
146+},
147+],
148+},
149+},
150+],
151+}),
152+},
153+release: vi.fn(async () => {}),
154+});
155+156+const provider = buildOpenRouterImageGenerationProvider();
157+const result = await provider.generateImage({
158+provider: "openrouter",
159+model: "google/gemini-3.1-flash-image-preview",
160+prompt: "turn this into watercolor",
161+inputImages: [{ buffer: Buffer.from("source-image"), mimeType: "image/png" }],
162+cfg: {} as never,
163+});
164+165+const body = postJsonRequestMock.mock.calls[0]?.[0].body as {
166+messages?: Array<{ content?: unknown }>;
167+};
168+expect(body.messages?.[0]?.content).toEqual([
169+{ type: "text", text: "turn this into watercolor" },
170+{
171+type: "image_url",
172+image_url: {
173+url: `data:image/png;base64,${Buffer.from("source-image").toString("base64")}`,
174+},
175+},
176+]);
177+expect(result.images[0]?.buffer.toString()).toBe("webp-one");
178+expect(result.images[0]?.mimeType).toBe("image/webp");
179+});
180+181+it("extracts image fallbacks from string content and raw b64 parts", () => {
182+const png = Buffer.from("png-inline").toString("base64");
183+const raw = Buffer.from("raw-inline").toString("base64");
184+const images = extractOpenRouterImagesFromResponse({
185+choices: [
186+{
187+message: {
188+content: `done data:image/png;base64,${png}`,
189+},
190+},
191+{
192+message: {
193+content: [{ b64_json: raw }],
194+},
195+},
196+],
197+});
198+199+expect(images.map((image) => image.buffer.toString())).toEqual(["png-inline", "raw-inline"]);
200+});
201+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。