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