ima2-gen 1.1.7 → 1.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (229) hide show
  1. package/README.md +56 -27
  2. package/bin/commands/annotate.js +137 -0
  3. package/bin/commands/annotate.ts +118 -0
  4. package/bin/commands/cancel.js +37 -33
  5. package/bin/commands/cancel.ts +45 -0
  6. package/bin/commands/canvas-versions.js +91 -0
  7. package/bin/commands/canvas-versions.ts +80 -0
  8. package/bin/commands/cardnews.js +293 -0
  9. package/bin/commands/cardnews.ts +248 -0
  10. package/bin/commands/comfy.js +63 -0
  11. package/bin/commands/comfy.ts +54 -0
  12. package/bin/commands/config.js +270 -0
  13. package/bin/commands/config.ts +265 -0
  14. package/bin/commands/edit.js +97 -72
  15. package/bin/commands/edit.ts +116 -0
  16. package/bin/commands/gen.js +140 -118
  17. package/bin/commands/gen.ts +176 -0
  18. package/bin/commands/history.js +164 -0
  19. package/bin/commands/history.ts +145 -0
  20. package/bin/commands/ls.js +60 -42
  21. package/bin/commands/ls.ts +60 -0
  22. package/bin/commands/metadata.js +45 -0
  23. package/bin/commands/metadata.ts +36 -0
  24. package/bin/commands/multimode.js +159 -0
  25. package/bin/commands/multimode.ts +146 -0
  26. package/bin/commands/node.js +176 -0
  27. package/bin/commands/node.ts +157 -0
  28. package/bin/commands/observability.js +201 -0
  29. package/bin/commands/observability.ts +176 -0
  30. package/bin/commands/ping.js +26 -20
  31. package/bin/commands/ping.ts +29 -0
  32. package/bin/commands/prompt.js +506 -0
  33. package/bin/commands/prompt.ts +421 -0
  34. package/bin/commands/ps.js +78 -71
  35. package/bin/commands/ps.ts +78 -0
  36. package/bin/commands/session.js +308 -0
  37. package/bin/commands/session.ts +265 -0
  38. package/bin/commands/show.js +75 -40
  39. package/bin/commands/show.ts +69 -0
  40. package/bin/ima2.js +324 -310
  41. package/bin/ima2.ts +444 -0
  42. package/bin/lib/args.js +75 -66
  43. package/bin/lib/args.ts +73 -0
  44. package/bin/lib/browser-id.js +15 -0
  45. package/bin/lib/browser-id.ts +16 -0
  46. package/bin/lib/client.js +91 -83
  47. package/bin/lib/client.ts +109 -0
  48. package/bin/lib/error-hints.js +14 -17
  49. package/bin/lib/error-hints.ts +23 -0
  50. package/bin/lib/files.js +26 -28
  51. package/bin/lib/files.ts +39 -0
  52. package/bin/lib/output.js +44 -42
  53. package/bin/lib/output.ts +58 -0
  54. package/bin/lib/platform.js +60 -56
  55. package/bin/lib/platform.ts +97 -0
  56. package/bin/lib/sse.js +73 -0
  57. package/bin/lib/sse.ts +73 -0
  58. package/bin/lib/star-prompt.js +69 -76
  59. package/bin/lib/star-prompt.ts +97 -0
  60. package/bin/lib/storage-doctor.js +34 -35
  61. package/bin/lib/storage-doctor.ts +38 -0
  62. package/config.js +147 -190
  63. package/config.ts +331 -0
  64. package/docs/API.md +48 -8
  65. package/docs/CLI.md +190 -0
  66. package/docs/FAQ.ko.md +5 -5
  67. package/docs/FAQ.md +5 -5
  68. package/docs/README.ja.md +71 -25
  69. package/docs/README.ko.md +61 -24
  70. package/docs/README.zh-CN.md +73 -27
  71. package/lib/assetLifecycle.js +130 -130
  72. package/lib/assetLifecycle.ts +142 -0
  73. package/lib/canvasVersionStore.js +135 -153
  74. package/lib/canvasVersionStore.ts +181 -0
  75. package/lib/cardNewsGenerator.js +127 -142
  76. package/lib/cardNewsGenerator.ts +162 -0
  77. package/lib/cardNewsJobStore.js +78 -84
  78. package/lib/cardNewsJobStore.ts +107 -0
  79. package/lib/cardNewsManifestStore.js +88 -93
  80. package/lib/cardNewsManifestStore.ts +112 -0
  81. package/lib/cardNewsPlanner.js +157 -152
  82. package/lib/cardNewsPlanner.ts +180 -0
  83. package/lib/cardNewsPlannerClient.js +101 -98
  84. package/lib/cardNewsPlannerClient.ts +114 -0
  85. package/lib/cardNewsPlannerPrompt.js +56 -56
  86. package/lib/cardNewsPlannerPrompt.ts +60 -0
  87. package/lib/cardNewsPlannerSchema.js +231 -223
  88. package/lib/cardNewsPlannerSchema.ts +259 -0
  89. package/lib/cardNewsRoleTemplateStore.js +39 -41
  90. package/lib/cardNewsRoleTemplateStore.ts +47 -0
  91. package/lib/cardNewsTemplateStore.js +171 -175
  92. package/lib/cardNewsTemplateStore.ts +210 -0
  93. package/lib/codexDetect.js +44 -47
  94. package/lib/codexDetect.ts +69 -0
  95. package/lib/comfyBridge.js +164 -184
  96. package/lib/comfyBridge.ts +214 -0
  97. package/lib/db.js +41 -51
  98. package/lib/db.ts +166 -0
  99. package/lib/errorClassify.js +62 -78
  100. package/lib/errorClassify.ts +100 -0
  101. package/lib/generationErrors.js +140 -103
  102. package/lib/generationErrors.ts +125 -0
  103. package/lib/historyList.js +149 -147
  104. package/lib/historyList.ts +164 -0
  105. package/lib/imageMetadata.js +86 -89
  106. package/lib/imageMetadata.ts +111 -0
  107. package/lib/imageMetadataStore.js +46 -51
  108. package/lib/imageMetadataStore.ts +67 -0
  109. package/lib/imageModels.js +38 -45
  110. package/lib/imageModels.ts +52 -0
  111. package/lib/inflight.js +131 -150
  112. package/lib/inflight.ts +204 -0
  113. package/lib/localImportStore.js +105 -0
  114. package/lib/localImportStore.ts +111 -0
  115. package/lib/logger.js +105 -112
  116. package/lib/logger.ts +150 -0
  117. package/lib/nodeStore.js +65 -64
  118. package/lib/nodeStore.ts +81 -0
  119. package/lib/oauthLauncher.js +61 -59
  120. package/lib/oauthLauncher.ts +64 -0
  121. package/lib/oauthNormalize.js +15 -19
  122. package/lib/oauthNormalize.ts +30 -0
  123. package/lib/oauthProxy.js +834 -832
  124. package/lib/oauthProxy.ts +995 -0
  125. package/lib/openDirectory.js +41 -40
  126. package/lib/openDirectory.ts +45 -0
  127. package/lib/pngInfo.js +18 -20
  128. package/lib/pngInfo.ts +26 -0
  129. package/lib/promptImport/curatedSources.js +135 -0
  130. package/lib/promptImport/curatedSources.ts +139 -0
  131. package/lib/promptImport/discoveryRegistry.js +218 -0
  132. package/lib/promptImport/discoveryRegistry.ts +236 -0
  133. package/lib/promptImport/errors.js +10 -10
  134. package/lib/promptImport/errors.ts +18 -0
  135. package/lib/promptImport/githubDiscovery.js +238 -0
  136. package/lib/promptImport/githubDiscovery.ts +248 -0
  137. package/lib/promptImport/githubFolder.js +302 -0
  138. package/lib/promptImport/githubFolder.ts +308 -0
  139. package/lib/promptImport/githubSource.js +194 -171
  140. package/lib/promptImport/githubSource.ts +239 -0
  141. package/lib/promptImport/gptImageHints.js +61 -0
  142. package/lib/promptImport/gptImageHints.ts +68 -0
  143. package/lib/promptImport/parsePromptCandidates.js +110 -112
  144. package/lib/promptImport/parsePromptCandidates.ts +153 -0
  145. package/lib/promptImport/promptIndex.js +230 -0
  146. package/lib/promptImport/promptIndex.ts +248 -0
  147. package/lib/promptImport/rankPromptCandidates.js +52 -0
  148. package/lib/promptImport/rankPromptCandidates.ts +49 -0
  149. package/lib/providerOptions.js +31 -0
  150. package/lib/providerOptions.ts +41 -0
  151. package/lib/referenceImageCompress.js +51 -62
  152. package/lib/referenceImageCompress.ts +75 -0
  153. package/lib/refs.js +93 -81
  154. package/lib/refs.ts +117 -0
  155. package/lib/requestLogger.js +32 -38
  156. package/lib/requestLogger.ts +48 -0
  157. package/lib/responsesImageAdapter.js +351 -0
  158. package/lib/responsesImageAdapter.ts +352 -0
  159. package/lib/runtimePorts.js +71 -73
  160. package/lib/runtimePorts.ts +93 -0
  161. package/lib/sessionStore.js +179 -230
  162. package/lib/sessionStore.ts +272 -0
  163. package/lib/storageMigration.js +247 -245
  164. package/lib/storageMigration.ts +284 -0
  165. package/lib/styleSheet.js +86 -90
  166. package/lib/styleSheet.ts +128 -0
  167. package/lib/systemTrash.js +18 -0
  168. package/lib/systemTrash.ts +20 -0
  169. package/package.json +26 -10
  170. package/routes/annotations.js +76 -79
  171. package/routes/annotations.ts +95 -0
  172. package/routes/canvasVersions.js +50 -54
  173. package/routes/canvasVersions.ts +64 -0
  174. package/routes/cardNews.js +158 -171
  175. package/routes/cardNews.ts +183 -0
  176. package/routes/comfy.js +23 -31
  177. package/routes/comfy.ts +39 -0
  178. package/routes/edit.js +183 -214
  179. package/routes/edit.ts +230 -0
  180. package/routes/generate.js +269 -291
  181. package/routes/generate.ts +309 -0
  182. package/routes/health.js +102 -107
  183. package/routes/health.ts +114 -0
  184. package/routes/history.js +136 -144
  185. package/routes/history.ts +153 -0
  186. package/routes/imageImport.js +33 -0
  187. package/routes/imageImport.ts +33 -0
  188. package/routes/index.js +18 -16
  189. package/routes/index.ts +35 -0
  190. package/routes/metadata.js +60 -64
  191. package/routes/metadata.ts +71 -0
  192. package/routes/multimode.js +228 -263
  193. package/routes/multimode.ts +280 -0
  194. package/routes/nodes.js +378 -424
  195. package/routes/nodes.ts +455 -0
  196. package/routes/promptImport.js +291 -152
  197. package/routes/promptImport.ts +354 -0
  198. package/routes/prompts.js +333 -360
  199. package/routes/prompts.ts +379 -0
  200. package/routes/sessions.js +277 -285
  201. package/routes/sessions.ts +292 -0
  202. package/routes/storage.js +29 -31
  203. package/routes/storage.ts +39 -0
  204. package/server.js +189 -196
  205. package/server.ts +235 -0
  206. package/ui/dist/.vite/manifest.json +101 -0
  207. package/ui/dist/assets/CardNewsWorkspace-BJOCey7Z.js +2 -0
  208. package/ui/dist/assets/NodeCanvas-BZV40eAE.css +1 -0
  209. package/ui/dist/assets/NodeCanvas-C3dzYNsk.js +7 -0
  210. package/ui/dist/assets/PromptImportDialog-Dqu1VpUh.js +2 -0
  211. package/ui/dist/assets/PromptImportDiscoverySection-Dg8T9X0L.js +1 -0
  212. package/ui/dist/assets/PromptImportFolderSection-DBaqsFO4.js +1 -0
  213. package/ui/dist/assets/PromptLibraryPanel-p5QqR97M.js +2 -0
  214. package/ui/dist/assets/SettingsWorkspace-B5bSAZ6u.js +1 -0
  215. package/ui/dist/assets/index-C9cXwiWE.js +25 -0
  216. package/ui/dist/assets/index-CGMIkZXn.css +1 -0
  217. package/ui/dist/assets/index-Cvld7dUZ.js +1 -0
  218. package/ui/dist/index.html +6 -3
  219. package/assets/screenshot.png +0 -0
  220. package/assets/screenshots/classic-generate-light.png +0 -0
  221. package/assets/screenshots/node-graph-branching.png +0 -0
  222. package/assets/screenshots/settings-oauth-generation.png +0 -0
  223. package/assets/screenshots/settings-workspace.png +0 -0
  224. package/assets/screenshots/style-sheet-editor.png +0 -0
  225. package/integrations/comfyui/ima2_gen_bridge/__pycache__/__init__.cpython-313.pyc +0 -0
  226. package/integrations/comfyui/ima2_gen_bridge/__pycache__/nodes.cpython-313.pyc +0 -0
  227. package/ui/dist/assets/index-DARPdT4Q.css +0 -1
  228. package/ui/dist/assets/index-ht80GMq4.js +0 -31
  229. package/ui/dist/assets/index-ht80GMq4.js.map +0 -1
@@ -1,281 +1,246 @@
1
1
  import { mkdir, writeFile } from "fs/promises";
2
2
  import { join } from "path";
3
3
  import { randomBytes } from "crypto";
4
- import { validateAndNormalizeRefs } from "../lib/refs.js";
4
+ import { summarizeReferencePayload, validateAndNormalizeRefs } from "../lib/refs.js";
5
5
  import { classifyUpstreamError } from "../lib/errorClassify.js";
6
6
  import { normalizeOAuthParams } from "../lib/oauthNormalize.js";
7
- import { normalizeImageModel, normalizeReasoningEffort } from "../lib/imageModels.js";
8
- import { generateMultimodeViaOAuth } from "../lib/oauthProxy.js";
7
+ import { resolveProviderOptions } from "../lib/providerOptions.js";
8
+ import { generateMultimodeViaResponses } from "../lib/responsesImageAdapter.js";
9
9
  import { startJob, finishJob } from "../lib/inflight.js";
10
10
  import { logEvent, logError } from "../lib/logger.js";
11
11
  import { embedImageMetadataBestEffort } from "../lib/imageMetadataStore.js";
12
-
13
12
  function sendSse(res, event, data) {
14
- res.write(`event: ${event}\n`);
15
- res.write(`data: ${JSON.stringify(data)}\n\n`);
13
+ res.write(`event: ${event}\n`);
14
+ res.write(`data: ${JSON.stringify(data)}\n\n`);
16
15
  }
17
-
18
16
  function validateModeration(ctx, moderation) {
19
- if (typeof moderation !== "string" || !ctx.config.oauth.validModeration.has(moderation)) {
20
- return { error: "moderation must be one of: auto, low" };
21
- }
22
- return { moderation };
17
+ if (typeof moderation !== "string" || !ctx.config.oauth.validModeration.has(moderation)) {
18
+ return { error: "moderation must be one of: auto, low" };
19
+ }
20
+ return { moderation };
23
21
  }
24
-
25
22
  function normalizeMaxImages(value) {
26
- return Math.min(8, Math.max(1, Math.trunc(Number(value) || 1)));
23
+ return Math.min(8, Math.max(1, Math.trunc(Number(value) || 1)));
27
24
  }
28
-
29
25
  function sequenceStatus(returned, requested) {
30
- if (returned <= 0) return "empty";
31
- if (returned < requested) return "partial";
32
- return "complete";
26
+ if (returned <= 0)
27
+ return "empty";
28
+ if (returned < requested)
29
+ return "partial";
30
+ return "complete";
33
31
  }
34
-
35
32
  export function registerMultimodeRoutes(app, ctx) {
36
- app.post("/api/generate/multimode", async (req, res) => {
37
- const requestId = typeof req.body?.requestId === "string" ? req.body.requestId : req.id;
38
- let finishStatus = "completed";
39
- let finishHttpStatus = 200;
40
- let finishErrorCode;
41
- let finishMeta = {};
42
-
43
- res.setHeader("Content-Type", "text/event-stream; charset=utf-8");
44
- res.setHeader("Cache-Control", "no-cache, no-transform");
45
- res.setHeader("Connection", "keep-alive");
46
- res.flushHeaders?.();
47
-
48
- try {
49
- const {
50
- prompt,
51
- quality: rawQuality = "medium",
52
- size = "1024x1024",
53
- format = "png",
54
- moderation = "low",
55
- provider = "auto",
56
- references = [],
57
- mode: promptMode = "auto",
58
- model: rawModel,
59
- reasoningEffort: rawReasoningEffort,
60
- webSearchEnabled: rawWebSearchEnabled = true,
61
- } = req.body;
62
- const maxImages = normalizeMaxImages(req.body?.maxImages);
63
- const normalizedPromptMode = promptMode === "direct" ? "direct" : "auto";
64
- const { quality, warnings: qualityWarnings } = normalizeOAuthParams({ provider, quality: rawQuality });
65
- const modelCheck = normalizeImageModel(ctx, rawModel);
66
- if (modelCheck.error) {
67
- finishStatus = "error";
68
- finishHttpStatus = modelCheck.status;
69
- finishErrorCode = modelCheck.code;
70
- sendSse(res, "error", { error: modelCheck.error, code: modelCheck.code, status: modelCheck.status, requestId });
71
- return;
72
- }
73
- const imageModel = modelCheck.model;
74
- const reasoningCheck = normalizeReasoningEffort(ctx, rawReasoningEffort);
75
- if (reasoningCheck.error) {
76
- finishStatus = "error";
77
- finishHttpStatus = reasoningCheck.status;
78
- finishErrorCode = reasoningCheck.code;
79
- sendSse(res, "error", { error: reasoningCheck.error, code: reasoningCheck.code, status: reasoningCheck.status, requestId });
80
- return;
81
- }
82
- const reasoningEffort = reasoningCheck.effort;
83
- const webSearchEnabled = rawWebSearchEnabled !== false;
84
- if (!prompt) {
85
- finishStatus = "error";
86
- finishHttpStatus = 400;
87
- finishErrorCode = "PROMPT_REQUIRED";
88
- sendSse(res, "error", { error: "Prompt is required", code: finishErrorCode, status: 400, requestId });
89
- return;
90
- }
91
- const moderationCheck = validateModeration(ctx, moderation);
92
- if (moderationCheck.error) {
93
- finishStatus = "error";
94
- finishHttpStatus = 400;
95
- finishErrorCode = "INVALID_MODERATION";
96
- sendSse(res, "error", { error: moderationCheck.error, code: finishErrorCode, status: 400, requestId });
97
- return;
98
- }
99
- if (provider === "api") {
100
- finishStatus = "error";
101
- finishHttpStatus = 403;
102
- finishErrorCode = "APIKEY_DISABLED";
103
- sendSse(res, "error", {
104
- error: "API key provider is disabled. Use OAuth (Codex login).",
105
- code: finishErrorCode,
106
- status: 403,
107
- requestId,
108
- });
109
- return;
110
- }
111
-
112
- const refCheck = validateAndNormalizeRefs(references);
113
- if (refCheck.error) {
114
- finishStatus = "error";
115
- finishHttpStatus = 400;
116
- finishErrorCode = refCheck.code;
117
- sendSse(res, "error", { error: refCheck.error, code: refCheck.code, status: 400, requestId });
118
- return;
119
- }
120
-
121
- startJob({
122
- requestId,
123
- kind: "multimode",
124
- prompt,
125
- meta: { kind: "multimode", quality, model: imageModel, size, maxImages },
126
- });
127
-
128
- logEvent("multimode", "request", {
129
- requestId,
130
- quality,
131
- model: imageModel,
132
- size,
133
- moderation,
134
- maxImages,
135
- refs: refCheck.refs.length,
136
- promptChars: typeof prompt === "string" ? prompt.length : 0,
137
- webSearchEnabled,
138
- });
139
-
140
- const startTime = Date.now();
141
- const mimeMap = { png: "image/png", jpeg: "image/jpeg", webp: "image/webp" };
142
- const mime = mimeMap[format] || "image/png";
143
- const sequenceId = `seq_${Date.now().toString(36)}_${randomBytes(4).toString("hex")}`;
144
- await mkdir(ctx.config.storage.generatedDir, { recursive: true });
145
-
146
- sendSse(res, "phase", { phase: "streaming", requestId, sequenceId, maxImages });
147
- const generated = await generateMultimodeViaOAuth(
148
- prompt,
149
- quality,
150
- size,
151
- moderation,
152
- refCheck.refDetails || refCheck.refs,
153
- requestId,
154
- normalizedPromptMode,
155
- ctx,
156
- {
157
- model: imageModel,
158
- maxImages,
159
- reasoningEffort,
160
- webSearchEnabled,
161
- onPartialImage: (partial) =>
162
- sendSse(res, "partial", {
163
- image: `data:${mime};base64,${partial.b64}`,
164
- requestId,
165
- sequenceId,
166
- index: partial.index,
167
- }),
168
- },
169
- );
170
-
171
- const returned = generated.images.length;
172
- const status = sequenceStatus(returned, maxImages);
173
- const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
174
- const images = [];
175
-
176
- for (const [index, image] of generated.images.entries()) {
177
- const rand = randomBytes(ctx.config.ids.generatedHexBytes).toString("hex");
178
- const filename = `${Date.now()}_${rand}_multimode_${index}.${format}`;
179
- const meta = {
180
- kind: "multimode-image",
181
- generationStrategy: "one-call-text-sequence",
182
- sequenceId,
183
- sequenceIndex: index + 1,
184
- sequenceTotalRequested: maxImages,
185
- sequenceTotalReturned: returned,
186
- sequenceStatus: status,
187
- stageLabel: String.fromCharCode(65 + index),
188
- requestId,
189
- prompt,
190
- userPrompt: prompt,
191
- revisedPrompt: image.revisedPrompt || null,
192
- promptMode: normalizedPromptMode,
193
- quality,
194
- size,
195
- format,
196
- moderation,
197
- model: imageModel,
198
- provider: "oauth",
199
- createdAt: Date.now(),
200
- usage: generated.usage || null,
201
- webSearchCalls: generated.webSearchCalls || 0,
202
- webSearchEnabled,
203
- refsCount: refCheck.refs.length,
204
- };
205
- const rawBuffer = Buffer.from(image.b64, "base64");
206
- const embedded = await embedImageMetadataBestEffort(rawBuffer, format, meta, {
207
- version: ctx.packageVersion,
208
- });
209
- await writeFile(join(ctx.config.storage.generatedDir, filename), embedded.buffer);
210
- await writeFile(join(ctx.config.storage.generatedDir, filename + ".json"), JSON.stringify(meta)).catch(() => {});
211
- const item = {
212
- image: `data:${mime};base64,${image.b64}`,
213
- filename,
214
- revisedPrompt: image.revisedPrompt || null,
215
- sequenceId,
216
- sequenceIndex: index + 1,
217
- sequenceTotalRequested: maxImages,
218
- sequenceTotalReturned: returned,
219
- sequenceStatus: status,
220
- };
221
- images.push(item);
222
- sendSse(res, "image", item);
223
- }
224
-
225
- finishMeta = { sequenceId, imageCount: returned, maxImages, status };
226
- finishHttpStatus = 200;
227
- sendSse(res, "done", {
228
- ok: true,
229
- requestId,
230
- sequenceId,
231
- requested: maxImages,
232
- returned,
233
- status,
234
- elapsed,
235
- images,
236
- provider: "oauth",
237
- quality,
238
- size,
239
- moderation,
240
- model: imageModel,
241
- usage: generated.usage || null,
242
- webSearchCalls: generated.webSearchCalls || 0,
243
- webSearchEnabled,
244
- warnings: qualityWarnings,
245
- extraIgnored: generated.extraIgnored || 0,
246
- promptMode: normalizedPromptMode,
247
- });
248
- logEvent("multimode", "saved", {
249
- requestId,
250
- sequenceId,
251
- imageCount: returned,
252
- maxImages,
253
- status,
254
- elapsedMs: Date.now() - startTime,
255
- });
256
- } catch (err) {
257
- const fallbackCode = err.code || classifyUpstreamError(err.message);
258
- finishStatus = "error";
259
- finishHttpStatus = err.status || 500;
260
- finishErrorCode = fallbackCode || "MULTIMODE_GENERATE_FAILED";
261
- logError("multimode", "error", err, { requestId, code: finishErrorCode });
262
- sendSse(res, "error", {
263
- error: err.message,
264
- code: finishErrorCode,
265
- status: finishHttpStatus,
266
- requestId,
267
- upstreamCode: err.upstreamCode || null,
268
- upstreamType: err.upstreamType || null,
269
- upstreamParam: err.upstreamParam || null,
270
- });
271
- } finally {
272
- finishJob(requestId, {
273
- status: finishStatus,
274
- httpStatus: finishHttpStatus,
275
- errorCode: finishErrorCode,
276
- meta: finishMeta,
277
- });
278
- res.end();
279
- }
280
- });
33
+ app.post("/api/generate/multimode", async (req, res) => {
34
+ const requestId = typeof req.body?.requestId === "string" ? req.body.requestId : req.id;
35
+ let finishStatus = "completed";
36
+ let finishHttpStatus = 200;
37
+ let finishErrorCode;
38
+ let finishMeta = {};
39
+ res.setHeader("Content-Type", "text/event-stream; charset=utf-8");
40
+ res.setHeader("Cache-Control", "no-cache, no-transform");
41
+ res.setHeader("Connection", "keep-alive");
42
+ res.flushHeaders?.();
43
+ try {
44
+ const { prompt, quality: rawQuality = "medium", size = "1024x1024", format = "png", moderation = "low", provider = "auto", references = [], mode: promptMode = "auto", model: rawModel, reasoningEffort: rawReasoningEffort, webSearchEnabled: rawWebSearchEnabled = true, } = req.body;
45
+ const maxImages = normalizeMaxImages(req.body?.maxImages);
46
+ const normalizedPromptMode = promptMode === "direct" ? "direct" : "auto";
47
+ const { quality, warnings: qualityWarnings } = normalizeOAuthParams({ provider, quality: rawQuality });
48
+ const providerOptions = resolveProviderOptions(ctx, {
49
+ provider,
50
+ rawModel,
51
+ rawReasoningEffort,
52
+ rawSize: size,
53
+ rawWebSearchEnabled,
54
+ });
55
+ if (providerOptions.error) {
56
+ finishStatus = "error";
57
+ finishHttpStatus = providerOptions.status;
58
+ finishErrorCode = providerOptions.code;
59
+ sendSse(res, "error", { error: providerOptions.error, code: providerOptions.code, status: providerOptions.status, requestId });
60
+ return;
61
+ }
62
+ const imageModel = providerOptions.model;
63
+ const reasoningEffort = providerOptions.reasoningEffort;
64
+ const effectiveSize = providerOptions.size;
65
+ const webSearchEnabled = providerOptions.webSearchEnabled;
66
+ const activeProvider = providerOptions.provider;
67
+ if (!prompt) {
68
+ finishStatus = "error";
69
+ finishHttpStatus = 400;
70
+ finishErrorCode = "PROMPT_REQUIRED";
71
+ sendSse(res, "error", { error: "Prompt is required", code: finishErrorCode, status: 400, requestId });
72
+ return;
73
+ }
74
+ const moderationCheck = validateModeration(ctx, moderation);
75
+ if (moderationCheck.error) {
76
+ finishStatus = "error";
77
+ finishHttpStatus = 400;
78
+ finishErrorCode = "INVALID_MODERATION";
79
+ sendSse(res, "error", { error: moderationCheck.error, code: finishErrorCode, status: 400, requestId });
80
+ return;
81
+ }
82
+ const refCheck = validateAndNormalizeRefs(references);
83
+ if (refCheck.error) {
84
+ finishStatus = "error";
85
+ finishHttpStatus = 400;
86
+ finishErrorCode = refCheck.code;
87
+ sendSse(res, "error", { error: refCheck.error, code: refCheck.code, status: 400, requestId });
88
+ return;
89
+ }
90
+ const referencePayload = summarizeReferencePayload(references);
91
+ startJob({
92
+ requestId,
93
+ kind: "multimode",
94
+ prompt,
95
+ meta: {
96
+ kind: "multimode",
97
+ quality,
98
+ model: imageModel,
99
+ size: effectiveSize,
100
+ maxImages,
101
+ refsCount: referencePayload.refsCount,
102
+ referenceBytes: referencePayload.referenceBytes,
103
+ referenceB64Chars: referencePayload.referenceB64Chars,
104
+ },
105
+ });
106
+ logEvent("multimode", "request", {
107
+ requestId,
108
+ quality,
109
+ model: imageModel,
110
+ size: effectiveSize,
111
+ moderation,
112
+ maxImages,
113
+ refs: refCheck.refs.length,
114
+ referenceBytes: referencePayload.referenceBytes,
115
+ promptChars: typeof prompt === "string" ? prompt.length : 0,
116
+ webSearchEnabled,
117
+ });
118
+ const startTime = Date.now();
119
+ const mimeMap = { png: "image/png", jpeg: "image/jpeg", webp: "image/webp" };
120
+ const mime = mimeMap[format] || "image/png";
121
+ const sequenceId = `seq_${Date.now().toString(36)}_${randomBytes(4).toString("hex")}`;
122
+ await mkdir(ctx.config.storage.generatedDir, { recursive: true });
123
+ sendSse(res, "phase", { phase: "streaming", requestId, sequenceId, maxImages });
124
+ const generated = await generateMultimodeViaResponses(activeProvider, prompt, quality, effectiveSize, moderation, refCheck.refDetails || refCheck.refs, requestId, normalizedPromptMode, ctx, {
125
+ model: imageModel,
126
+ maxImages,
127
+ reasoningEffort,
128
+ webSearchEnabled,
129
+ onPartialImage: (partial) => sendSse(res, "partial", {
130
+ image: `data:${mime};base64,${partial.b64}`,
131
+ requestId,
132
+ sequenceId,
133
+ index: partial.index,
134
+ }),
135
+ });
136
+ const returned = generated.images.length;
137
+ const status = sequenceStatus(returned, maxImages);
138
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
139
+ const images = [];
140
+ for (const [index, image] of generated.images.entries()) {
141
+ const rand = randomBytes(ctx.config.ids.generatedHexBytes).toString("hex");
142
+ const filename = `${Date.now()}_${rand}_multimode_${index}.${format}`;
143
+ const meta = {
144
+ kind: "multimode-image",
145
+ generationStrategy: "one-call-text-sequence",
146
+ sequenceId,
147
+ sequenceIndex: index + 1,
148
+ sequenceTotalRequested: maxImages,
149
+ sequenceTotalReturned: returned,
150
+ sequenceStatus: status,
151
+ stageLabel: String.fromCharCode(65 + index),
152
+ requestId,
153
+ prompt,
154
+ userPrompt: prompt,
155
+ revisedPrompt: image.revisedPrompt || null,
156
+ promptMode: normalizedPromptMode,
157
+ quality,
158
+ size: effectiveSize,
159
+ format,
160
+ moderation,
161
+ model: imageModel,
162
+ provider: activeProvider,
163
+ createdAt: Date.now(),
164
+ usage: generated.usage || null,
165
+ webSearchCalls: generated.webSearchCalls || 0,
166
+ webSearchEnabled,
167
+ refsCount: refCheck.refs.length,
168
+ };
169
+ const rawBuffer = Buffer.from(image.b64, "base64");
170
+ const embedded = await embedImageMetadataBestEffort(rawBuffer, format, meta, {
171
+ version: ctx.packageVersion,
172
+ });
173
+ await writeFile(join(ctx.config.storage.generatedDir, filename), embedded.buffer);
174
+ await writeFile(join(ctx.config.storage.generatedDir, filename + ".json"), JSON.stringify(meta)).catch(() => { });
175
+ const item = {
176
+ image: `data:${mime};base64,${image.b64}`,
177
+ filename,
178
+ revisedPrompt: image.revisedPrompt || null,
179
+ sequenceId,
180
+ sequenceIndex: index + 1,
181
+ sequenceTotalRequested: maxImages,
182
+ sequenceTotalReturned: returned,
183
+ sequenceStatus: status,
184
+ };
185
+ images.push(item);
186
+ sendSse(res, "image", item);
187
+ }
188
+ finishMeta = { sequenceId, imageCount: returned, maxImages, status };
189
+ finishHttpStatus = 200;
190
+ sendSse(res, "done", {
191
+ ok: true,
192
+ requestId,
193
+ sequenceId,
194
+ requested: maxImages,
195
+ returned,
196
+ status,
197
+ elapsed,
198
+ images,
199
+ provider: activeProvider,
200
+ quality,
201
+ size: effectiveSize,
202
+ moderation,
203
+ model: imageModel,
204
+ usage: generated.usage || null,
205
+ webSearchCalls: generated.webSearchCalls || 0,
206
+ webSearchEnabled,
207
+ warnings: qualityWarnings,
208
+ extraIgnored: generated.extraIgnored || 0,
209
+ promptMode: normalizedPromptMode,
210
+ });
211
+ logEvent("multimode", "saved", {
212
+ requestId,
213
+ sequenceId,
214
+ imageCount: returned,
215
+ maxImages,
216
+ status,
217
+ elapsedMs: Date.now() - startTime,
218
+ });
219
+ }
220
+ catch (err) {
221
+ const fallbackCode = err.code || classifyUpstreamError(err.message);
222
+ finishStatus = "error";
223
+ finishHttpStatus = err.status || 500;
224
+ finishErrorCode = fallbackCode || "MULTIMODE_GENERATE_FAILED";
225
+ logError("multimode", "error", err, { requestId, code: finishErrorCode });
226
+ sendSse(res, "error", {
227
+ error: err.message,
228
+ code: finishErrorCode,
229
+ status: finishHttpStatus,
230
+ requestId,
231
+ upstreamCode: err.upstreamCode || null,
232
+ upstreamType: err.upstreamType || null,
233
+ upstreamParam: err.upstreamParam || null,
234
+ });
235
+ }
236
+ finally {
237
+ finishJob(requestId, {
238
+ status: finishStatus,
239
+ httpStatus: finishHttpStatus,
240
+ errorCode: finishErrorCode,
241
+ meta: finishMeta,
242
+ });
243
+ res.end();
244
+ }
245
+ });
281
246
  }