ima2-gen 1.1.8 → 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 (230) 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 -243
  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 +87 -93
  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 +125 -129
  130. package/lib/promptImport/curatedSources.ts +139 -0
  131. package/lib/promptImport/discoveryRegistry.js +185 -203
  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 +209 -219
  136. package/lib/promptImport/githubDiscovery.ts +248 -0
  137. package/lib/promptImport/githubFolder.js +253 -259
  138. package/lib/promptImport/githubFolder.ts +308 -0
  139. package/lib/promptImport/githubSource.js +189 -200
  140. package/lib/promptImport/githubSource.ts +239 -0
  141. package/lib/promptImport/gptImageHints.js +49 -56
  142. package/lib/promptImport/gptImageHints.ts +68 -0
  143. package/lib/promptImport/parsePromptCandidates.js +108 -123
  144. package/lib/promptImport/parsePromptCandidates.ts +153 -0
  145. package/lib/promptImport/promptIndex.js +190 -208
  146. package/lib/promptImport/promptIndex.ts +248 -0
  147. package/lib/promptImport/rankPromptCandidates.js +46 -43
  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 +27 -27
  187. package/routes/imageImport.ts +33 -0
  188. package/routes/index.js +17 -17
  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 +284 -324
  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 +2 -2
  219. package/assets/phase-a-bg-cleanup-test.png +0 -0
  220. package/assets/screenshot.png +0 -0
  221. package/assets/screenshots/classic-generate-light.png +0 -0
  222. package/assets/screenshots/node-graph-branching.png +0 -0
  223. package/assets/screenshots/settings-oauth-generation.png +0 -0
  224. package/assets/screenshots/settings-workspace.png +0 -0
  225. package/assets/screenshots/style-sheet-editor.png +0 -0
  226. package/integrations/comfyui/ima2_gen_bridge/__pycache__/__init__.cpython-313.pyc +0 -0
  227. package/integrations/comfyui/ima2_gen_bridge/__pycache__/nodes.cpython-313.pyc +0 -0
  228. package/ui/dist/assets/index-BDffwmLs.css +0 -1
  229. package/ui/dist/assets/index-D0fdHLkJ.js +0 -31
  230. package/ui/dist/assets/index-D0fdHLkJ.js.map +0 -1
@@ -0,0 +1,455 @@
1
+ import { mkdir } from "fs/promises";
2
+ import {
3
+ newNodeId,
4
+ saveNode,
5
+ loadNodeB64,
6
+ loadNodeMeta,
7
+ loadAssetB64,
8
+ } from "../lib/nodeStore.js";
9
+ import { startJob, finishJob } from "../lib/inflight.js";
10
+ import { summarizeReferencePayload, validateAndNormalizeRefs } from "../lib/refs.js";
11
+ import { classifyUpstreamError } from "../lib/errorClassify.js";
12
+ import { normalizeOAuthParams } from "../lib/oauthNormalize.js";
13
+ import { resolveProviderOptions } from "../lib/providerOptions.js";
14
+ import { generateViaResponses, editViaResponses } from "../lib/responsesImageAdapter.js";
15
+ import { isNonRetryableGenerationError, normalizeGenerationFailure } from "../lib/generationErrors.js";
16
+ import { logEvent, logError } from "../lib/logger.js";
17
+
18
+ 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 };
23
+ }
24
+
25
+ function wantsSse(req) {
26
+ const accept = typeof req.headers.accept === "string" ? req.headers.accept : "";
27
+ return accept.includes("text/event-stream");
28
+ }
29
+
30
+ function writeSse(res, event, data) {
31
+ res.write(`event: ${event}\n`);
32
+ res.write(`data: ${JSON.stringify(data)}\n\n`);
33
+ }
34
+
35
+ function writeNodeError(res, status, code, message, parentNodeId, details = {}) {
36
+ if (res.headersSent) {
37
+ writeSse(res, "error", {
38
+ error: { code, message },
39
+ parentNodeId,
40
+ status,
41
+ ...details,
42
+ });
43
+ res.end();
44
+ return;
45
+ }
46
+ res.status(status).json({
47
+ error: { code, message },
48
+ parentNodeId,
49
+ status,
50
+ ...details,
51
+ });
52
+ }
53
+
54
+ function dataUrlFromB64(format, b64) {
55
+ return `data:image/${format === "jpeg" ? "jpeg" : format};base64,${b64}`;
56
+ }
57
+
58
+ export function registerNodeRoutes(app, ctx) {
59
+ app.post("/api/node/generate", async (req, res) => {
60
+ const body = req.body || {};
61
+ const streamResponse = wantsSse(req);
62
+ const parentNodeId = body.parentNodeId ?? null;
63
+ const requestId = typeof body.requestId === "string" ? body.requestId : req.id;
64
+ const sessionId = typeof body.sessionId === "string" ? body.sessionId : null;
65
+ const clientNodeId = typeof body.clientNodeId === "string" ? body.clientNodeId : null;
66
+ let finishMeta = {};
67
+ let finishStatus = "completed";
68
+ let finishHttpStatus;
69
+ let finishErrorCode;
70
+ const referencePayload = summarizeReferencePayload(body.references);
71
+ startJob({
72
+ requestId,
73
+ kind: "node",
74
+ prompt: body.prompt,
75
+ meta: {
76
+ kind: "node",
77
+ sessionId,
78
+ parentNodeId,
79
+ clientNodeId,
80
+ refsCount: referencePayload.refsCount,
81
+ referenceBytes: referencePayload.referenceBytes,
82
+ referenceB64Chars: referencePayload.referenceB64Chars,
83
+ },
84
+ });
85
+
86
+ try {
87
+ const {
88
+ prompt,
89
+ quality: rawQuality = "medium",
90
+ size = "1024x1024",
91
+ format = "png",
92
+ moderation = "low",
93
+ references = [],
94
+ externalSrc = null,
95
+ mode: promptMode = "auto",
96
+ contextMode: rawContextMode = "parent-plus-refs",
97
+ searchMode: rawSearchMode = "on",
98
+ model: rawModel,
99
+ reasoningEffort: rawReasoningEffort,
100
+ } = body;
101
+ const { provider = "oauth" } = body;
102
+ const { quality, warnings: qualityWarnings } = normalizeOAuthParams({ provider, quality: rawQuality });
103
+ const normalizedPromptMode = promptMode === "direct" ? "direct" : "auto";
104
+ const contextMode = ["parent-plus-refs", "parent-only", "ancestry"].includes(rawContextMode)
105
+ ? rawContextMode
106
+ : "parent-plus-refs";
107
+ const searchMode = ["off", "auto", "on"].includes(rawSearchMode) ? rawSearchMode : "on";
108
+ const providerOptions = resolveProviderOptions(ctx, {
109
+ provider,
110
+ rawModel,
111
+ rawReasoningEffort,
112
+ rawSize: size,
113
+ rawWebSearchEnabled: body.webSearchEnabled,
114
+ searchMode,
115
+ });
116
+ if (providerOptions.error) {
117
+ finishStatus = "error";
118
+ finishHttpStatus = providerOptions.status;
119
+ finishErrorCode = providerOptions.code;
120
+ return res.status(providerOptions.status).json({
121
+ error: { code: providerOptions.code, message: providerOptions.error },
122
+ parentNodeId,
123
+ });
124
+ }
125
+ const imageModel = providerOptions.model;
126
+ const reasoningEffort = providerOptions.reasoningEffort;
127
+ const effectiveSize = providerOptions.size;
128
+ const webSearchEnabled = providerOptions.webSearchEnabled;
129
+ const activeProvider = providerOptions.provider;
130
+ if (contextMode === "ancestry") {
131
+ finishStatus = "error";
132
+ finishHttpStatus = 400;
133
+ finishErrorCode = "CONTEXT_MODE_UNSUPPORTED";
134
+ return res.status(400).json({
135
+ error: { code: "CONTEXT_MODE_UNSUPPORTED", message: "Ancestry context is not supported yet." },
136
+ parentNodeId,
137
+ });
138
+ }
139
+
140
+ if (!prompt || typeof prompt !== "string") {
141
+ finishStatus = "error";
142
+ finishHttpStatus = 400;
143
+ finishErrorCode = "INVALID_PROMPT";
144
+ return res.status(400).json({
145
+ error: { code: "INVALID_PROMPT", message: "Prompt is required" },
146
+ parentNodeId,
147
+ });
148
+ }
149
+ const refCheck = validateAndNormalizeRefs(references);
150
+ if (refCheck.error) {
151
+ finishStatus = "error";
152
+ finishHttpStatus = 400;
153
+ finishErrorCode = refCheck.code;
154
+ return res.status(400).json({
155
+ error: { code: refCheck.code, message: refCheck.error },
156
+ code: refCheck.code,
157
+ parentNodeId,
158
+ });
159
+ }
160
+ const moderationCheck = validateModeration(ctx, moderation);
161
+ if (moderationCheck.error) {
162
+ finishStatus = "error";
163
+ finishHttpStatus = 400;
164
+ finishErrorCode = "INVALID_MODERATION";
165
+ return res.status(400).json({
166
+ error: { code: "INVALID_MODERATION", message: moderationCheck.error },
167
+ parentNodeId,
168
+ });
169
+ }
170
+
171
+ const startTime = Date.now();
172
+ let parentB64 = null;
173
+ if (parentNodeId) {
174
+ parentB64 = await loadNodeB64(ctx.rootDir, `${parentNodeId}.png`, ctx.config.storage.generatedDir);
175
+ } else if (typeof externalSrc === "string" && externalSrc.length > 0) {
176
+ parentB64 = await loadAssetB64(ctx.rootDir, externalSrc, ctx.config.storage.generatedDir);
177
+ }
178
+ const operation = parentB64 ? "edit" : "generate";
179
+ const referenceDiagnostics = refCheck.referenceDiagnostics || [];
180
+ const generateReferenceDiagnostics = operation === "generate" ? referenceDiagnostics : [];
181
+ const referenceMismatchCount = generateReferenceDiagnostics.filter((ref) => ref.warnings?.includes("mime_mismatch")).length;
182
+ const refsForRequest = contextMode === "parent-only" ? [] : (refCheck.refDetails || refCheck.refs);
183
+ const parentImagePresent = !!parentB64;
184
+ const inputImageCount = (parentImagePresent ? 1 : 0) + refsForRequest.length;
185
+ logEvent("node", "request", {
186
+ requestId,
187
+ operation,
188
+ sessionId,
189
+ parentNodeId,
190
+ clientNodeId,
191
+ quality,
192
+ model: imageModel,
193
+ size: effectiveSize,
194
+ moderation,
195
+ refs: refsForRequest.length,
196
+ referenceBytes: referencePayload.referenceBytes,
197
+ referenceMismatchCount,
198
+ refDetectedMimes: [...new Set(generateReferenceDiagnostics.map((ref) => ref.detectedMime).filter(Boolean))].join(","),
199
+ refDeclaredMimes: [...new Set(generateReferenceDiagnostics.map((ref) => ref.declaredMime).filter(Boolean))].join(","),
200
+ inputImageCount,
201
+ parentImagePresent,
202
+ contextMode,
203
+ searchMode,
204
+ webSearchEnabled,
205
+ promptChars: prompt.length,
206
+ promptMode: normalizedPromptMode,
207
+ });
208
+
209
+ if (streamResponse) {
210
+ res.writeHead(200, {
211
+ "Content-Type": "text/event-stream; charset=utf-8",
212
+ "Cache-Control": "no-cache, no-transform",
213
+ Connection: "keep-alive",
214
+ });
215
+ writeSse(res, "phase", { requestId, phase: "streaming" });
216
+ }
217
+
218
+ let b64, usage, webSearchCalls = 0, revisedPrompt = null;
219
+ const MAX_RETRIES = 1;
220
+ let lastErr;
221
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
222
+ try {
223
+ logEvent("node", "attempt", {
224
+ requestId,
225
+ attempt,
226
+ operation,
227
+ sessionId,
228
+ parentNodeId,
229
+ clientNodeId,
230
+ model: imageModel,
231
+ moderation,
232
+ quality,
233
+ size: effectiveSize,
234
+ refs: refsForRequest.length,
235
+ inputImageCount,
236
+ parentImagePresent,
237
+ contextMode,
238
+ searchMode,
239
+ webSearchEnabled,
240
+ });
241
+ const r = parentB64
242
+ ? await editViaResponses(activeProvider, prompt, parentB64, quality, effectiveSize, moderation, normalizedPromptMode, ctx, requestId, {
243
+ model: imageModel,
244
+ references: refsForRequest,
245
+ searchMode,
246
+ reasoningEffort,
247
+ webSearchEnabled,
248
+ })
249
+ : await generateViaResponses(
250
+ activeProvider,
251
+ prompt,
252
+ quality,
253
+ effectiveSize,
254
+ moderation,
255
+ refsForRequest,
256
+ requestId,
257
+ normalizedPromptMode,
258
+ ctx,
259
+ {
260
+ model: imageModel,
261
+ reasoningEffort,
262
+ webSearchEnabled,
263
+ partialImages: streamResponse ? 2 : 0,
264
+ onPartialImage: streamResponse
265
+ ? (partial) =>
266
+ writeSse(res, "partial", {
267
+ requestId,
268
+ image: dataUrlFromB64(format, partial.b64),
269
+ index: partial.index,
270
+ })
271
+ : null,
272
+ },
273
+ );
274
+ if (r.b64) {
275
+ b64 = r.b64;
276
+ usage = r.usage;
277
+ webSearchCalls = r.webSearchCalls || 0;
278
+ revisedPrompt = r.revisedPrompt || null;
279
+ break;
280
+ }
281
+ lastErr = new Error("Empty response (safety refusal)");
282
+ } catch (e) {
283
+ lastErr = e;
284
+ if (isNonRetryableGenerationError(e)) break;
285
+ }
286
+ if (attempt < MAX_RETRIES) {
287
+ logEvent("node", "retry", {
288
+ requestId,
289
+ attempt: attempt + 1,
290
+ operation,
291
+ parentNodeId,
292
+ clientNodeId,
293
+ errorCode: lastErr?.code,
294
+ errorEventType: lastErr?.eventType,
295
+ errorEventCount: lastErr?.eventCount,
296
+ });
297
+ }
298
+ }
299
+
300
+ if (!b64) {
301
+ const finalErr = normalizeGenerationFailure(lastErr, {
302
+ safetyMessage: lastErr?.message || "Empty response after retry",
303
+ });
304
+ finishStatus = "error";
305
+ finishHttpStatus = finalErr.status || 500;
306
+ finishErrorCode = finalErr.code || "NODE_GEN_FAILED";
307
+ logEvent("node", "final_error", {
308
+ requestId,
309
+ operation,
310
+ finalCode: finishErrorCode,
311
+ upstreamCode: lastErr?.upstreamCode || lastErr?.code,
312
+ errorEventType: lastErr?.eventType,
313
+ errorEventCount: lastErr?.eventCount,
314
+ diagnosticReason: lastErr?.diagnosticReason,
315
+ retryKind: lastErr?.retryKind,
316
+ referencesDroppedOnRetry: lastErr?.referencesDroppedOnRetry,
317
+ attempts: MAX_RETRIES + 1,
318
+ outerHttpAlreadyCommitted: res.headersSent,
319
+ sseErrorSent: streamResponse,
320
+ });
321
+ return writeNodeError(
322
+ res,
323
+ finishHttpStatus,
324
+ finishErrorCode,
325
+ finalErr.message,
326
+ parentNodeId,
327
+ {
328
+ upstreamCode: lastErr?.upstreamCode || lastErr?.code || null,
329
+ upstreamType: lastErr?.upstreamType || null,
330
+ upstreamParam: lastErr?.upstreamParam || null,
331
+ errorEventType: lastErr?.eventType || null,
332
+ errorEventCount: lastErr?.eventCount ?? null,
333
+ diagnosticReason: finalErr.diagnosticReason || lastErr?.diagnosticReason || null,
334
+ retryKind: finalErr.retryKind || lastErr?.retryKind || null,
335
+ referencesDroppedOnRetry: finalErr.referencesDroppedOnRetry ?? lastErr?.referencesDroppedOnRetry ?? null,
336
+ refsCount: finalErr.refsCount ?? lastErr?.refsCount ?? null,
337
+ inputImageCount: finalErr.inputImageCount ?? lastErr?.inputImageCount ?? null,
338
+ },
339
+ );
340
+ }
341
+
342
+ const nodeId = newNodeId();
343
+ const elapsed = +((Date.now() - startTime) / 1000).toFixed(1);
344
+ const meta = {
345
+ nodeId,
346
+ parentNodeId,
347
+ sessionId,
348
+ clientNodeId,
349
+ prompt,
350
+ userPrompt: prompt,
351
+ revisedPrompt,
352
+ promptMode: normalizedPromptMode,
353
+ options: { quality, size: effectiveSize, format, moderation },
354
+ model: imageModel,
355
+ createdAt: Date.now(),
356
+ createdAtIso: new Date().toISOString(),
357
+ elapsed,
358
+ usage: usage || null,
359
+ webSearchCalls,
360
+ webSearchEnabled,
361
+ contextMode,
362
+ searchMode,
363
+ provider: activeProvider,
364
+ kind: parentB64 ? "edit" : "generate",
365
+ requestId,
366
+ refsCount: refsForRequest.length,
367
+ quality,
368
+ size: effectiveSize,
369
+ format,
370
+ moderation,
371
+ };
372
+ await mkdir(ctx.config.storage.generatedDir, { recursive: true });
373
+ const { filename } = await saveNode(ctx.rootDir, {
374
+ nodeId,
375
+ b64,
376
+ meta,
377
+ ext: format,
378
+ generatedDir: ctx.config.storage.generatedDir,
379
+ });
380
+ finishMeta = { nodeId, filename, imageChars: b64.length };
381
+ finishHttpStatus = 200;
382
+ logEvent("node", "saved", {
383
+ requestId,
384
+ nodeId,
385
+ filename,
386
+ imageChars: b64.length,
387
+ elapsedMs: Date.now() - startTime,
388
+ });
389
+
390
+ const payload = {
391
+ nodeId,
392
+ parentNodeId,
393
+ requestId,
394
+ image: dataUrlFromB64(format, b64),
395
+ filename,
396
+ url: `/generated/${filename}`,
397
+ elapsed,
398
+ usage,
399
+ webSearchCalls,
400
+ webSearchEnabled,
401
+ provider: activeProvider,
402
+ model: imageModel,
403
+ size: effectiveSize,
404
+ moderation,
405
+ refsCount: refsForRequest.length,
406
+ contextMode,
407
+ searchMode,
408
+ warnings: qualityWarnings,
409
+ revisedPrompt,
410
+ promptMode: normalizedPromptMode,
411
+ };
412
+
413
+ if (streamResponse) {
414
+ writeSse(res, "done", payload);
415
+ res.end();
416
+ } else {
417
+ res.json(payload);
418
+ }
419
+ } catch (err) {
420
+ const code = err.code || classifyUpstreamError(err.message) || "NODE_GEN_FAILED";
421
+ finishStatus = "error";
422
+ finishHttpStatus = err.status || 500;
423
+ finishErrorCode = code;
424
+ logError("node", "error", err, { requestId, code, parentNodeId, sessionId, clientNodeId });
425
+ writeNodeError(res, err.status || 500, code, err.message, parentNodeId, {
426
+ upstreamCode: err.upstreamCode || null,
427
+ upstreamType: err.upstreamType || null,
428
+ upstreamParam: err.upstreamParam || null,
429
+ });
430
+ } finally {
431
+ finishJob(requestId, {
432
+ status: finishStatus,
433
+ httpStatus: finishHttpStatus,
434
+ errorCode: finishErrorCode,
435
+ meta: finishMeta,
436
+ });
437
+ }
438
+ });
439
+
440
+ app.get("/api/node/:nodeId", async (req, res) => {
441
+ try {
442
+ const { nodeId } = req.params;
443
+ const meta = await loadNodeMeta(ctx.rootDir, nodeId, "png", ctx.config.storage.generatedDir);
444
+ if (!meta) {
445
+ return res.status(404).json({ error: { code: "NODE_NOT_FOUND", message: "Node metadata missing" } });
446
+ }
447
+ const ext = meta?.options?.format || meta?.format || "png";
448
+ res.json({ nodeId, meta, url: `/generated/${nodeId}.${ext}` });
449
+ } catch (err) {
450
+ res.status(err.status || 500).json({
451
+ error: { code: err.code || "NODE_FETCH_FAILED", message: err.message },
452
+ });
453
+ }
454
+ });
455
+ }