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,308 +1,286 @@
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 { generateViaOAuth } from "../lib/oauthProxy.js";
7
+ import { resolveProviderOptions } from "../lib/providerOptions.js";
8
+ import { generateViaResponses } from "../lib/responsesImageAdapter.js";
9
9
  import { isNonRetryableGenerationError, normalizeGenerationFailure } from "../lib/generationErrors.js";
10
10
  import { startJob, finishJob } from "../lib/inflight.js";
11
11
  import { logEvent, logError } from "../lib/logger.js";
12
12
  import { embedImageMetadataBestEffort } from "../lib/imageMetadataStore.js";
13
-
14
13
  function validateModeration(ctx, moderation) {
15
- if (typeof moderation !== "string" || !ctx.config.oauth.validModeration.has(moderation)) {
16
- return { error: "moderation must be one of: auto, low" };
17
- }
18
- return { moderation };
14
+ if (typeof moderation !== "string" || !ctx.config.oauth.validModeration.has(moderation)) {
15
+ return { error: "moderation must be one of: auto, low" };
16
+ }
17
+ return { moderation };
19
18
  }
20
-
21
19
  export function registerGenerateRoutes(app, ctx) {
22
- app.post("/api/generate", async (req, res) => {
23
- const requestId = typeof req.body?.requestId === "string" ? req.body.requestId : req.id;
24
- let finishStatus = "completed";
25
- let finishHttpStatus;
26
- let finishErrorCode;
27
- let finishMeta = {};
28
- try {
29
- const sessionId = typeof req.body?.sessionId === "string" ? req.body.sessionId : null;
30
- const clientNodeId = typeof req.body?.clientNodeId === "string" ? req.body.clientNodeId : null;
31
- const {
32
- prompt,
33
- quality: rawQuality = "medium",
34
- size = "1024x1024",
35
- format = "png",
36
- moderation = "low",
37
- provider = "auto",
38
- n = 1,
39
- references = [],
40
- mode: promptMode = "auto",
41
- model: rawModel,
42
- reasoningEffort: rawReasoningEffort,
43
- webSearchEnabled: rawWebSearchEnabled = true,
44
- } = req.body;
45
- const { quality, warnings: qualityWarnings } = normalizeOAuthParams({ provider, quality: rawQuality });
46
- const modelCheck = normalizeImageModel(ctx, rawModel);
47
- if (modelCheck.error) {
48
- finishStatus = "error";
49
- finishHttpStatus = modelCheck.status;
50
- finishErrorCode = modelCheck.code;
51
- return res.status(modelCheck.status).json({ error: modelCheck.error, code: modelCheck.code });
52
- }
53
- const imageModel = modelCheck.model;
54
- const reasoningCheck = normalizeReasoningEffort(ctx, rawReasoningEffort);
55
- if (reasoningCheck.error) {
56
- finishStatus = "error";
57
- finishHttpStatus = reasoningCheck.status;
58
- finishErrorCode = reasoningCheck.code;
59
- return res.status(reasoningCheck.status).json({ error: reasoningCheck.error, code: reasoningCheck.code });
60
- }
61
- const reasoningEffort = reasoningCheck.effort;
62
- const webSearchEnabled = rawWebSearchEnabled !== false;
63
- const normalizedPromptMode = promptMode === "direct" ? "direct" : "auto";
64
-
65
- if (!prompt) return res.status(400).json({ error: "Prompt is required" });
66
- const moderationCheck = validateModeration(ctx, moderation);
67
- if (moderationCheck.error) return res.status(400).json({ error: moderationCheck.error });
68
- const count = Math.min(Math.max(parseInt(n) || 1, 1), 8);
69
-
70
- startJob({
71
- requestId,
72
- kind: "classic",
73
- prompt,
74
- meta: {
75
- kind: "classic",
76
- sessionId,
77
- parentNodeId: null,
78
- clientNodeId,
79
- quality,
80
- model: imageModel,
81
- size,
82
- n: count,
83
- },
84
- });
85
-
86
- const refCheck = validateAndNormalizeRefs(references);
87
- if (refCheck.error) {
88
- finishStatus = "error";
89
- finishHttpStatus = 400;
90
- finishErrorCode = refCheck.code;
91
- return res.status(400).json({ error: refCheck.error, code: refCheck.code });
92
- }
93
-
94
- if (provider === "api") {
95
- finishStatus = "error";
96
- finishHttpStatus = 403;
97
- finishErrorCode = "APIKEY_DISABLED";
98
- return res.status(403).json({ error: "API key provider is disabled. Use OAuth (Codex login).", code: "APIKEY_DISABLED" });
99
- }
100
- const client = req.get("x-ima2-client") || "ui";
101
- const referenceDiagnostics = refCheck.referenceDiagnostics || [];
102
- const referenceMismatchCount = referenceDiagnostics.filter((ref) => ref.warnings?.includes("mime_mismatch")).length;
103
- logEvent("generate", "request", {
104
- requestId,
105
- client,
106
- provider: "oauth",
107
- quality,
108
- model: imageModel,
109
- size,
110
- moderation,
111
- n: count,
112
- refs: refCheck.refs.length,
113
- referenceMismatchCount,
114
- refDetectedMimes: [...new Set(referenceDiagnostics.map((ref) => ref.detectedMime).filter(Boolean))].join(","),
115
- refDeclaredMimes: [...new Set(referenceDiagnostics.map((ref) => ref.declaredMime).filter(Boolean))].join(","),
116
- sessionId,
117
- clientNodeId,
118
- promptChars: typeof prompt === "string" ? prompt.length : 0,
119
- promptMode: normalizedPromptMode,
120
- webSearchEnabled,
121
- });
122
- const startTime = Date.now();
123
-
124
- const mimeMap = { png: "image/png", jpeg: "image/jpeg", webp: "image/webp" };
125
- const mime = mimeMap[format] || "image/png";
126
- await mkdir(ctx.config.storage.generatedDir, { recursive: true });
127
-
128
- const generateOne = async () => {
129
- const MAX_RETRIES = 1;
130
- let lastErr;
131
- for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
132
- try {
133
- const r = await generateViaOAuth(
134
- prompt,
135
- quality,
136
- size,
137
- moderation,
138
- refCheck.refDetails || refCheck.refs,
139
- requestId,
140
- normalizedPromptMode,
141
- ctx,
142
- { model: imageModel, reasoningEffort, webSearchEnabled },
143
- );
144
- if (r.b64) return r;
145
- lastErr = new Error("Empty response (safety refusal)");
146
- } catch (e) {
147
- lastErr = e;
148
- if (isNonRetryableGenerationError(e)) break;
149
- }
150
- if (attempt < MAX_RETRIES) {
151
- logEvent("generate", "retry", { requestId, attempt: attempt + 1, errorCode: lastErr?.code });
152
- }
153
- }
154
- throw normalizeGenerationFailure(lastErr, {
155
- safetyMessage: "Content generation refused after retries",
156
- });
157
- };
158
-
159
- const results = await Promise.allSettled(Array.from({ length: count }, generateOne));
160
- const images = [];
161
- let totalUsage = null;
162
- let totalWebSearchCalls = 0;
163
- for (const r of results) {
164
- if (r.status === "fulfilled" && r.value.b64) {
165
- const rand = randomBytes(ctx.config.ids.generatedHexBytes).toString("hex");
166
- const filename = `${Date.now()}_${rand}_${images.length}.${format}`;
167
- const meta = {
168
- kind: "classic",
169
- requestId,
170
- sessionId,
171
- clientNodeId,
172
- prompt,
173
- userPrompt: prompt,
174
- revisedPrompt: r.value.revisedPrompt || null,
175
- promptMode: normalizedPromptMode,
176
- quality,
177
- size,
178
- format,
179
- moderation,
180
- model: imageModel,
181
- provider: "oauth",
182
- createdAt: Date.now(),
183
- usage: r.value.usage || null,
184
- webSearchCalls: r.value.webSearchCalls || 0,
185
- webSearchEnabled,
186
- refsCount: refCheck.refs.length,
187
- };
188
- const rawBuffer = Buffer.from(r.value.b64, "base64");
189
- const embedded = await embedImageMetadataBestEffort(rawBuffer, format, meta, {
190
- version: ctx.packageVersion,
191
- });
192
- if (!embedded.embedded) {
193
- logEvent("generate", "metadata_embed_skipped", {
194
- requestId,
195
- filename,
196
- code: embedded.code,
197
- warning: embedded.warning,
20
+ app.post("/api/generate", async (req, res) => {
21
+ const requestId = typeof req.body?.requestId === "string" ? req.body.requestId : req.id;
22
+ let finishStatus = "completed";
23
+ let finishHttpStatus;
24
+ let finishErrorCode;
25
+ let finishMeta = {};
26
+ try {
27
+ const sessionId = typeof req.body?.sessionId === "string" ? req.body.sessionId : null;
28
+ const clientNodeId = typeof req.body?.clientNodeId === "string" ? req.body.clientNodeId : null;
29
+ const { prompt, quality: rawQuality = "medium", size = "1024x1024", format = "png", moderation = "low", provider = "auto", n = 1, references = [], mode: promptMode = "auto", model: rawModel, reasoningEffort: rawReasoningEffort, webSearchEnabled: rawWebSearchEnabled = true, } = req.body;
30
+ const { quality, warnings: qualityWarnings } = normalizeOAuthParams({ provider, quality: rawQuality });
31
+ const providerOptions = resolveProviderOptions(ctx, {
32
+ provider,
33
+ rawModel,
34
+ rawReasoningEffort,
35
+ rawSize: size,
36
+ rawWebSearchEnabled,
198
37
  });
199
- }
200
- await writeFile(join(ctx.config.storage.generatedDir, filename), embedded.buffer);
201
- await writeFile(join(ctx.config.storage.generatedDir, filename + ".json"), JSON.stringify(meta)).catch(() => {});
202
- images.push({
203
- image: `data:${mime};base64,${r.value.b64}`,
204
- filename,
205
- revisedPrompt: r.value.revisedPrompt || null,
206
- });
207
- if (r.value.usage) {
208
- if (!totalUsage) totalUsage = { ...r.value.usage };
209
- else Object.keys(r.value.usage).forEach((k) => {
210
- if (typeof r.value.usage[k] === "number") totalUsage[k] = (totalUsage[k] || 0) + r.value.usage[k];
38
+ if (providerOptions.error) {
39
+ finishStatus = "error";
40
+ finishHttpStatus = providerOptions.status;
41
+ finishErrorCode = providerOptions.code;
42
+ return res.status(providerOptions.status).json({ error: providerOptions.error, code: providerOptions.code });
43
+ }
44
+ const imageModel = providerOptions.model;
45
+ const reasoningEffort = providerOptions.reasoningEffort;
46
+ const effectiveSize = providerOptions.size;
47
+ const webSearchEnabled = providerOptions.webSearchEnabled;
48
+ const activeProvider = providerOptions.provider;
49
+ const normalizedPromptMode = promptMode === "direct" ? "direct" : "auto";
50
+ if (!prompt)
51
+ return res.status(400).json({ error: "Prompt is required" });
52
+ const moderationCheck = validateModeration(ctx, moderation);
53
+ if (moderationCheck.error)
54
+ return res.status(400).json({ error: moderationCheck.error });
55
+ const count = Math.min(Math.max(parseInt(n) || 1, 1), 8);
56
+ const referencePayload = summarizeReferencePayload(references);
57
+ startJob({
58
+ requestId,
59
+ kind: "classic",
60
+ prompt,
61
+ meta: {
62
+ kind: "classic",
63
+ sessionId,
64
+ parentNodeId: null,
65
+ clientNodeId,
66
+ quality,
67
+ model: imageModel,
68
+ size: effectiveSize,
69
+ n: count,
70
+ refsCount: referencePayload.refsCount,
71
+ referenceBytes: referencePayload.referenceBytes,
72
+ referenceB64Chars: referencePayload.referenceB64Chars,
73
+ },
211
74
  });
212
- }
213
- if (typeof r.value.webSearchCalls === "number") totalWebSearchCalls += r.value.webSearchCalls;
214
- } else if (r.status === "rejected") {
215
- logError("generate", "parallel_failed", r.reason, { requestId });
75
+ const refCheck = validateAndNormalizeRefs(references);
76
+ if (refCheck.error) {
77
+ finishStatus = "error";
78
+ finishHttpStatus = 400;
79
+ finishErrorCode = refCheck.code;
80
+ return res.status(400).json({ error: refCheck.error, code: refCheck.code });
81
+ }
82
+ const client = req.get("x-ima2-client") || "ui";
83
+ const referenceDiagnostics = refCheck.referenceDiagnostics || [];
84
+ const referenceMismatchCount = referenceDiagnostics.filter((ref) => ref.warnings?.includes("mime_mismatch")).length;
85
+ logEvent("generate", "request", {
86
+ requestId,
87
+ client,
88
+ provider: activeProvider,
89
+ quality,
90
+ model: imageModel,
91
+ size: effectiveSize,
92
+ moderation,
93
+ n: count,
94
+ refs: refCheck.refs.length,
95
+ referenceBytes: referencePayload.referenceBytes,
96
+ referenceMismatchCount,
97
+ refDetectedMimes: [...new Set(referenceDiagnostics.map((ref) => ref.detectedMime).filter(Boolean))].join(","),
98
+ refDeclaredMimes: [...new Set(referenceDiagnostics.map((ref) => ref.declaredMime).filter(Boolean))].join(","),
99
+ sessionId,
100
+ clientNodeId,
101
+ promptChars: typeof prompt === "string" ? prompt.length : 0,
102
+ promptMode: normalizedPromptMode,
103
+ webSearchEnabled,
104
+ });
105
+ const startTime = Date.now();
106
+ const mimeMap = { png: "image/png", jpeg: "image/jpeg", webp: "image/webp" };
107
+ const mime = mimeMap[format] || "image/png";
108
+ await mkdir(ctx.config.storage.generatedDir, { recursive: true });
109
+ const generateOne = async () => {
110
+ const MAX_RETRIES = 1;
111
+ let lastErr;
112
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
113
+ try {
114
+ const r = await generateViaResponses(activeProvider, prompt, quality, effectiveSize, moderation, refCheck.refDetails || refCheck.refs, requestId, normalizedPromptMode, ctx, { model: imageModel, reasoningEffort, webSearchEnabled });
115
+ if (r.b64)
116
+ return r;
117
+ lastErr = new Error("Empty response (safety refusal)");
118
+ }
119
+ catch (e) {
120
+ lastErr = e;
121
+ if (isNonRetryableGenerationError(e))
122
+ break;
123
+ }
124
+ if (attempt < MAX_RETRIES) {
125
+ logEvent("generate", "retry", { requestId, attempt: attempt + 1, errorCode: lastErr?.code });
126
+ }
127
+ }
128
+ throw normalizeGenerationFailure(lastErr, {
129
+ safetyMessage: "Content generation refused after retries",
130
+ });
131
+ };
132
+ const results = await Promise.allSettled(Array.from({ length: count }, generateOne));
133
+ const images = [];
134
+ let totalUsage = null;
135
+ let totalWebSearchCalls = 0;
136
+ for (const r of results) {
137
+ if (r.status === "fulfilled" && r.value.b64) {
138
+ const rand = randomBytes(ctx.config.ids.generatedHexBytes).toString("hex");
139
+ const filename = `${Date.now()}_${rand}_${images.length}.${format}`;
140
+ const meta = {
141
+ kind: "classic",
142
+ requestId,
143
+ sessionId,
144
+ clientNodeId,
145
+ prompt,
146
+ userPrompt: prompt,
147
+ revisedPrompt: r.value.revisedPrompt || null,
148
+ promptMode: normalizedPromptMode,
149
+ quality,
150
+ size: effectiveSize,
151
+ format,
152
+ moderation,
153
+ model: imageModel,
154
+ provider: activeProvider,
155
+ createdAt: Date.now(),
156
+ usage: r.value.usage || null,
157
+ webSearchCalls: r.value.webSearchCalls || 0,
158
+ webSearchEnabled,
159
+ refsCount: refCheck.refs.length,
160
+ };
161
+ const rawBuffer = Buffer.from(r.value.b64, "base64");
162
+ const embedded = await embedImageMetadataBestEffort(rawBuffer, format, meta, {
163
+ version: ctx.packageVersion,
164
+ });
165
+ if (!embedded.embedded) {
166
+ logEvent("generate", "metadata_embed_skipped", {
167
+ requestId,
168
+ filename,
169
+ code: embedded.code,
170
+ warning: embedded.warning,
171
+ });
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
+ images.push({
176
+ image: `data:${mime};base64,${r.value.b64}`,
177
+ filename,
178
+ revisedPrompt: r.value.revisedPrompt || null,
179
+ });
180
+ if (r.value.usage) {
181
+ if (!totalUsage)
182
+ totalUsage = { ...r.value.usage };
183
+ else
184
+ Object.keys(r.value.usage).forEach((k) => {
185
+ if (typeof r.value.usage[k] === "number")
186
+ totalUsage[k] = (totalUsage[k] || 0) + r.value.usage[k];
187
+ });
188
+ }
189
+ if (typeof r.value.webSearchCalls === "number")
190
+ totalWebSearchCalls += r.value.webSearchCalls;
191
+ }
192
+ else if (r.status === "rejected") {
193
+ logError("generate", "parallel_failed", r.reason, { requestId });
194
+ }
195
+ }
196
+ if (images.length === 0) {
197
+ const firstErr = results.find((r) => r.status === "rejected")?.reason;
198
+ if (firstErr?.code) {
199
+ const status = firstErr.status || 500;
200
+ finishStatus = "error";
201
+ finishHttpStatus = status;
202
+ finishErrorCode = firstErr.code;
203
+ return res.status(status).json({
204
+ error: firstErr.message,
205
+ code: firstErr.code,
206
+ upstreamCode: firstErr.upstreamCode || null,
207
+ upstreamType: firstErr.upstreamType || null,
208
+ upstreamParam: firstErr.upstreamParam || null,
209
+ diagnosticReason: firstErr.diagnosticReason || null,
210
+ retryKind: firstErr.retryKind || null,
211
+ referencesDroppedOnRetry: firstErr.referencesDroppedOnRetry ?? null,
212
+ errorEventCount: firstErr.eventCount ?? null,
213
+ requestId,
214
+ });
215
+ }
216
+ finishStatus = "error";
217
+ finishHttpStatus = 500;
218
+ finishErrorCode = "GENERATE_ALL_FAILED";
219
+ return res.status(500).json({ error: "All generation attempts failed" });
220
+ }
221
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
222
+ const firstRevised = images[0]?.revisedPrompt || null;
223
+ const extra = {
224
+ usage: totalUsage,
225
+ provider: activeProvider,
226
+ webSearchCalls: totalWebSearchCalls,
227
+ quality,
228
+ size: effectiveSize,
229
+ moderation,
230
+ model: imageModel,
231
+ warnings: qualityWarnings,
232
+ revisedPrompt: firstRevised,
233
+ promptMode: normalizedPromptMode,
234
+ webSearchEnabled,
235
+ };
236
+ if (count === 1) {
237
+ finishHttpStatus = 200;
238
+ finishMeta = { filenames: [images[0].filename], imageCount: 1 };
239
+ logEvent("generate", "saved", {
240
+ requestId,
241
+ imageCount: 1,
242
+ elapsedMs: Date.now() - startTime,
243
+ filename: images[0].filename,
244
+ });
245
+ res.json({ image: images[0].image, elapsed, filename: images[0].filename, requestId, ...extra });
246
+ }
247
+ else {
248
+ finishHttpStatus = 200;
249
+ finishMeta = { filenames: images.map((image) => image.filename), imageCount: images.length };
250
+ logEvent("generate", "saved", {
251
+ requestId,
252
+ imageCount: images.length,
253
+ elapsedMs: Date.now() - startTime,
254
+ });
255
+ res.json({ images, elapsed, count: images.length, requestId, ...extra });
256
+ }
216
257
  }
217
- }
218
-
219
- if (images.length === 0) {
220
- const firstErr = results.find((r) => r.status === "rejected")?.reason;
221
- if (firstErr?.code) {
222
- const status = firstErr.status || 500;
223
- finishStatus = "error";
224
- finishHttpStatus = status;
225
- finishErrorCode = firstErr.code;
226
- return res.status(status).json({
227
- error: firstErr.message,
228
- code: firstErr.code,
229
- upstreamCode: firstErr.upstreamCode || null,
230
- upstreamType: firstErr.upstreamType || null,
231
- upstreamParam: firstErr.upstreamParam || null,
232
- diagnosticReason: firstErr.diagnosticReason || null,
233
- retryKind: firstErr.retryKind || null,
234
- referencesDroppedOnRetry: firstErr.referencesDroppedOnRetry ?? null,
235
- errorEventCount: firstErr.eventCount ?? null,
236
- requestId,
237
- });
258
+ catch (err) {
259
+ const fallbackCode = err.code || classifyUpstreamError(err.message);
260
+ finishStatus = "error";
261
+ finishHttpStatus = err.status || 500;
262
+ finishErrorCode = fallbackCode || "GENERATE_FAILED";
263
+ logError("generate", "error", err, { requestId, code: finishErrorCode });
264
+ res.status(err.status || 500).json({
265
+ error: err.message,
266
+ code: fallbackCode,
267
+ upstreamCode: err.upstreamCode || null,
268
+ upstreamType: err.upstreamType || null,
269
+ upstreamParam: err.upstreamParam || null,
270
+ diagnosticReason: err.diagnosticReason || null,
271
+ retryKind: err.retryKind || null,
272
+ referencesDroppedOnRetry: err.referencesDroppedOnRetry ?? null,
273
+ errorEventCount: err.eventCount ?? null,
274
+ requestId,
275
+ });
238
276
  }
239
- finishStatus = "error";
240
- finishHttpStatus = 500;
241
- finishErrorCode = "GENERATE_ALL_FAILED";
242
- return res.status(500).json({ error: "All generation attempts failed" });
243
- }
244
-
245
- const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
246
- const firstRevised = images[0]?.revisedPrompt || null;
247
- const extra = {
248
- usage: totalUsage,
249
- provider: "oauth",
250
- webSearchCalls: totalWebSearchCalls,
251
- quality,
252
- size,
253
- moderation,
254
- model: imageModel,
255
- warnings: qualityWarnings,
256
- revisedPrompt: firstRevised,
257
- promptMode: normalizedPromptMode,
258
- webSearchEnabled,
259
- };
260
-
261
- if (count === 1) {
262
- finishHttpStatus = 200;
263
- finishMeta = { filenames: [images[0].filename], imageCount: 1 };
264
- logEvent("generate", "saved", {
265
- requestId,
266
- imageCount: 1,
267
- elapsedMs: Date.now() - startTime,
268
- filename: images[0].filename,
269
- });
270
- res.json({ image: images[0].image, elapsed, filename: images[0].filename, requestId, ...extra });
271
- } else {
272
- finishHttpStatus = 200;
273
- finishMeta = { filenames: images.map((image) => image.filename), imageCount: images.length };
274
- logEvent("generate", "saved", {
275
- requestId,
276
- imageCount: images.length,
277
- elapsedMs: Date.now() - startTime,
278
- });
279
- res.json({ images, elapsed, count: images.length, requestId, ...extra });
280
- }
281
- } catch (err) {
282
- const fallbackCode = err.code || classifyUpstreamError(err.message);
283
- finishStatus = "error";
284
- finishHttpStatus = err.status || 500;
285
- finishErrorCode = fallbackCode || "GENERATE_FAILED";
286
- logError("generate", "error", err, { requestId, code: finishErrorCode });
287
- res.status(err.status || 500).json({
288
- error: err.message,
289
- code: fallbackCode,
290
- upstreamCode: err.upstreamCode || null,
291
- upstreamType: err.upstreamType || null,
292
- upstreamParam: err.upstreamParam || null,
293
- diagnosticReason: err.diagnosticReason || null,
294
- retryKind: err.retryKind || null,
295
- referencesDroppedOnRetry: err.referencesDroppedOnRetry ?? null,
296
- errorEventCount: err.eventCount ?? null,
297
- requestId,
298
- });
299
- } finally {
300
- finishJob(requestId, {
301
- status: finishStatus,
302
- httpStatus: finishHttpStatus,
303
- errorCode: finishErrorCode,
304
- meta: finishMeta,
305
- });
306
- }
307
- });
277
+ finally {
278
+ finishJob(requestId, {
279
+ status: finishStatus,
280
+ httpStatus: finishHttpStatus,
281
+ errorCode: finishErrorCode,
282
+ meta: finishMeta,
283
+ });
284
+ }
285
+ });
308
286
  }