ima2-gen 1.1.6 → 1.1.7

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 (41) hide show
  1. package/.env.example +5 -0
  2. package/README.md +3 -0
  3. package/config.js +58 -0
  4. package/docs/FAQ.ko.md +20 -0
  5. package/docs/FAQ.md +20 -0
  6. package/docs/README.ko.md +3 -0
  7. package/docs/README.zh-CN.md +3 -0
  8. package/integrations/comfyui/ima2_gen_bridge/README.md +88 -0
  9. package/integrations/comfyui/ima2_gen_bridge/__init__.py +3 -0
  10. package/integrations/comfyui/ima2_gen_bridge/__pycache__/__init__.cpython-313.pyc +0 -0
  11. package/integrations/comfyui/ima2_gen_bridge/__pycache__/nodes.cpython-313.pyc +0 -0
  12. package/integrations/comfyui/ima2_gen_bridge/nodes.py +238 -0
  13. package/lib/canvasVersionStore.js +181 -0
  14. package/lib/cardNewsPlannerClient.js +4 -2
  15. package/lib/comfyBridge.js +214 -0
  16. package/lib/db.js +14 -0
  17. package/lib/historyList.js +4 -0
  18. package/lib/imageMetadata.js +4 -0
  19. package/lib/imageModels.js +20 -0
  20. package/lib/oauthProxy.js +88 -38
  21. package/lib/pngInfo.js +26 -0
  22. package/lib/promptImport/errors.js +16 -0
  23. package/lib/promptImport/githubSource.js +205 -0
  24. package/lib/promptImport/parsePromptCandidates.js +140 -0
  25. package/package.json +3 -2
  26. package/routes/annotations.js +95 -0
  27. package/routes/canvasVersions.js +64 -0
  28. package/routes/comfy.js +39 -0
  29. package/routes/edit.js +73 -4
  30. package/routes/generate.js +16 -2
  31. package/routes/index.js +8 -0
  32. package/routes/multimode.js +18 -1
  33. package/routes/nodes.js +25 -3
  34. package/routes/promptImport.js +175 -0
  35. package/ui/dist/assets/index-DARPdT4Q.css +1 -0
  36. package/ui/dist/assets/index-ht80GMq4.js +31 -0
  37. package/ui/dist/assets/index-ht80GMq4.js.map +1 -0
  38. package/ui/dist/index.html +2 -2
  39. package/ui/dist/assets/index-3X-6VjbF.css +0 -1
  40. package/ui/dist/assets/index-DPSq9qEs.js +0 -31
  41. package/ui/dist/assets/index-DPSq9qEs.js.map +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ima2-gen",
3
- "version": "1.1.6",
3
+ "version": "1.1.7",
4
4
  "description": "Local OAuth image generation studio with classic and node workflows",
5
5
  "type": "module",
6
6
  "bin": {
@@ -18,7 +18,7 @@
18
18
  "test:package-install": "node --test tests/package-install-smoke.mjs",
19
19
  "setup": "node bin/ima2.js setup",
20
20
  "prepublishOnly": "npm test && npm run build && npm run test:package-install && npm run lint:pkg",
21
- "lint:pkg": "node -e \"const p=require('./package.json'); const req=['name','version','bin']; for(const k of req){if(!p[k])throw new Error('missing '+k)} const mustInclude=['bin/','lib/','routes/','server.js']; for(const f of mustInclude){if(!p.files.includes(f))throw new Error('files[] must include '+f)}\"",
21
+ "lint:pkg": "node -e \"const p=require('./package.json'); const req=['name','version','bin']; for(const k of req){if(!p[k])throw new Error('missing '+k)} const mustInclude=['bin/','lib/','routes/','integrations/','server.js']; for(const f of mustInclude){if(!p.files.includes(f))throw new Error('files[] must include '+f)}\"",
22
22
  "release:patch": "npm version patch && npm publish && git push origin main --tags",
23
23
  "release:minor": "npm version minor && npm publish && git push origin main --tags",
24
24
  "release:major": "npm version major && npm publish && git push origin main --tags"
@@ -39,6 +39,7 @@
39
39
  "bin/",
40
40
  "lib/",
41
41
  "routes/",
42
+ "integrations/",
42
43
  "ui/dist/",
43
44
  "docs/",
44
45
  "assets/",
@@ -0,0 +1,95 @@
1
+ import { getDb } from "../lib/db.js";
2
+
3
+ const MAX_ANNOTATION_PAYLOAD_CHARS = 256 * 1024;
4
+
5
+ function getBrowserId(req) {
6
+ const browserId = req.headers["x-ima2-browser-id"];
7
+ return typeof browserId === "string" && browserId.trim() ? browserId.trim() : null;
8
+ }
9
+
10
+ function isSafeFilename(filename) {
11
+ return (
12
+ typeof filename === "string" &&
13
+ filename.length > 0 &&
14
+ filename.length <= 240 &&
15
+ !filename.includes("..") &&
16
+ !filename.startsWith("/") &&
17
+ !filename.includes("\\")
18
+ );
19
+ }
20
+
21
+ function normalizePayload(value) {
22
+ const payload = value?.annotations ?? value;
23
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
24
+ return { error: "annotations payload is required" };
25
+ }
26
+ const paths = Array.isArray(payload.paths) ? payload.paths : [];
27
+ const boxes = Array.isArray(payload.boxes) ? payload.boxes : [];
28
+ const memos = Array.isArray(payload.memos) ? payload.memos : [];
29
+ const normalized = { paths, boxes, memos };
30
+ const text = JSON.stringify(normalized);
31
+ if (text.length > MAX_ANNOTATION_PAYLOAD_CHARS) {
32
+ return { error: "annotations payload is too large" };
33
+ }
34
+ return { payload: normalized, text };
35
+ }
36
+
37
+ export function registerAnnotationRoutes(app) {
38
+ app.get("/api/annotations/:filename", (req, res) => {
39
+ try {
40
+ const browserId = getBrowserId(req);
41
+ const filename = decodeURIComponent(req.params.filename);
42
+ if (!browserId) return res.status(400).json({ error: "X-Ima2-Browser-Id header is required" });
43
+ if (!isSafeFilename(filename)) return res.status(400).json({ error: "invalid filename" });
44
+
45
+ const row = getDb()
46
+ .prepare("SELECT payload FROM image_annotations WHERE browser_id = ? AND filename = ?")
47
+ .get(browserId, filename);
48
+ const annotations = row ? JSON.parse(row.payload) : null;
49
+ res.json({ annotations });
50
+ } catch (err) {
51
+ res.status(500).json({ error: err.message });
52
+ }
53
+ });
54
+
55
+ app.put("/api/annotations/:filename", (req, res) => {
56
+ try {
57
+ const browserId = getBrowserId(req);
58
+ const filename = decodeURIComponent(req.params.filename);
59
+ if (!browserId) return res.status(400).json({ error: "X-Ima2-Browser-Id header is required" });
60
+ if (!isSafeFilename(filename)) return res.status(400).json({ error: "invalid filename" });
61
+
62
+ const normalized = normalizePayload(req.body);
63
+ if (normalized.error) return res.status(400).json({ error: normalized.error });
64
+
65
+ const id = `${browserId}:${filename}`;
66
+ getDb().prepare(`
67
+ INSERT INTO image_annotations (id, browser_id, filename, payload, schema_version, updated_at)
68
+ VALUES (?, ?, ?, ?, 1, unixepoch())
69
+ ON CONFLICT(browser_id, filename) DO UPDATE SET
70
+ payload = excluded.payload,
71
+ schema_version = excluded.schema_version,
72
+ updated_at = unixepoch()
73
+ `).run(id, browserId, filename, normalized.text);
74
+ res.json({ ok: true });
75
+ } catch (err) {
76
+ res.status(500).json({ error: err.message });
77
+ }
78
+ });
79
+
80
+ app.delete("/api/annotations/:filename", (req, res) => {
81
+ try {
82
+ const browserId = getBrowserId(req);
83
+ const filename = decodeURIComponent(req.params.filename);
84
+ if (!browserId) return res.status(400).json({ error: "X-Ima2-Browser-Id header is required" });
85
+ if (!isSafeFilename(filename)) return res.status(400).json({ error: "invalid filename" });
86
+
87
+ getDb()
88
+ .prepare("DELETE FROM image_annotations WHERE browser_id = ? AND filename = ?")
89
+ .run(browserId, filename);
90
+ res.json({ ok: true });
91
+ } catch (err) {
92
+ res.status(500).json({ error: err.message });
93
+ }
94
+ });
95
+ }
@@ -0,0 +1,64 @@
1
+ import express from "express";
2
+ import { createCanvasVersion, updateCanvasVersion } from "../lib/canvasVersionStore.js";
3
+
4
+ function decodeHeader(value) {
5
+ if (typeof value !== "string" || !value) return null;
6
+ try {
7
+ return decodeURIComponent(value);
8
+ } catch {
9
+ return value;
10
+ }
11
+ }
12
+
13
+ function getRequestBuffer(req) {
14
+ return Buffer.isBuffer(req.body) ? req.body : Buffer.alloc(0);
15
+ }
16
+
17
+ function getPrompt(req) {
18
+ return decodeHeader(req.headers["x-ima2-canvas-prompt"]);
19
+ }
20
+
21
+ export function registerCanvasVersionRoutes(app, ctx) {
22
+ const rawPng = express.raw({ type: "image/png", limit: ctx.config.server.bodyLimit });
23
+
24
+ app.post("/api/canvas-versions", rawPng, async (req, res) => {
25
+ try {
26
+ const sourceFilename =
27
+ typeof req.query.sourceFilename === "string"
28
+ ? req.query.sourceFilename
29
+ : decodeHeader(req.headers["x-ima2-canvas-source-filename"]);
30
+ const item = await createCanvasVersion(ctx, {
31
+ sourceFilename,
32
+ prompt: getPrompt(req),
33
+ buffer: getRequestBuffer(req),
34
+ });
35
+ res.status(201).json({ item });
36
+ } catch (err) {
37
+ res.status(err.status || 500).json({
38
+ error: err.message,
39
+ code: err.code || "CANVAS_VERSION_SAVE_FAILED",
40
+ });
41
+ }
42
+ });
43
+
44
+ app.put("/api/canvas-versions/:filename", rawPng, async (req, res) => {
45
+ try {
46
+ const filename = decodeURIComponent(req.params.filename);
47
+ const sourceFilename =
48
+ typeof req.query.sourceFilename === "string"
49
+ ? req.query.sourceFilename
50
+ : decodeHeader(req.headers["x-ima2-canvas-source-filename"]);
51
+ const item = await updateCanvasVersion(ctx, filename, {
52
+ sourceFilename,
53
+ prompt: getPrompt(req),
54
+ buffer: getRequestBuffer(req),
55
+ });
56
+ res.json({ item });
57
+ } catch (err) {
58
+ res.status(err.status || 500).json({
59
+ error: err.message,
60
+ code: err.code || "CANVAS_VERSION_SAVE_FAILED",
61
+ });
62
+ }
63
+ });
64
+ }
@@ -0,0 +1,39 @@
1
+ import { exportImageToComfy, isComfyBridgeError } from "../lib/comfyBridge.js";
2
+
3
+ const ALLOWED_BODY_KEYS = new Set(["filename"]);
4
+
5
+ function hasExactBodyShape(body) {
6
+ if (!body || typeof body !== "object" || Array.isArray(body)) return false;
7
+ const keys = Object.keys(body);
8
+ return keys.length === 1 && ALLOWED_BODY_KEYS.has(keys[0]) && typeof body.filename === "string";
9
+ }
10
+
11
+ function errorPayload(code, message) {
12
+ return {
13
+ ok: false,
14
+ error: { code, message },
15
+ };
16
+ }
17
+
18
+ export function registerComfyRoutes(app, ctx) {
19
+ app.post("/api/comfy/export-image", async (req, res) => {
20
+ try {
21
+ if (!hasExactBodyShape(req.body)) {
22
+ return res.status(400).json(errorPayload(
23
+ "COMFY_IMAGE_INVALID",
24
+ "Request body must contain exactly one filename.",
25
+ ));
26
+ }
27
+ const result = await exportImageToComfy(ctx, { filename: req.body.filename });
28
+ return res.json(result);
29
+ } catch (error) {
30
+ if (isComfyBridgeError(error)) {
31
+ return res.status(error.status).json(errorPayload(error.code, error.message));
32
+ }
33
+ return res.status(502).json(errorPayload(
34
+ "COMFY_UPLOAD_FAILED",
35
+ "Could not upload image to ComfyUI.",
36
+ ));
37
+ }
38
+ });
39
+ }
package/routes/edit.js CHANGED
@@ -4,9 +4,10 @@ import { randomBytes } from "crypto";
4
4
  import { editViaOAuth } from "../lib/oauthProxy.js";
5
5
  import { classifyUpstreamError } from "../lib/errorClassify.js";
6
6
  import { normalizeOAuthParams } from "../lib/oauthNormalize.js";
7
- import { normalizeImageModel } from "../lib/imageModels.js";
7
+ import { normalizeImageModel, normalizeReasoningEffort } from "../lib/imageModels.js";
8
8
  import { startJob, finishJob } from "../lib/inflight.js";
9
9
  import { logEvent, logError } from "../lib/logger.js";
10
+ import { hasPngAlphaChannel, parsePngInfo } from "../lib/pngInfo.js";
10
11
 
11
12
  function validateModeration(ctx, moderation) {
12
13
  if (typeof moderation !== "string" || !ctx.config.oauth.validModeration.has(moderation)) {
@@ -15,6 +16,49 @@ function validateModeration(ctx, moderation) {
15
16
  return { moderation };
16
17
  }
17
18
 
19
+ const MAX_EDIT_MASK_BYTES = 16 * 1024 * 1024;
20
+ const BASE64_RE = /^[A-Za-z0-9+/]+={0,2}$/;
21
+
22
+ function stripPngDataUrl(value) {
23
+ if (typeof value !== "string") return "";
24
+ return value.replace(/^data:image\/png;base64,/, "");
25
+ }
26
+
27
+ function decodePngDataUrl(value, invalidCode, pngCode) {
28
+ const b64 = stripPngDataUrl(value).replace(/\s+/g, "");
29
+ if (!b64 || b64.length % 4 !== 0 || !BASE64_RE.test(b64)) {
30
+ return { error: "image must be valid base64", code: invalidCode };
31
+ }
32
+ const buffer = Buffer.from(b64, "base64");
33
+ if (buffer.length === 0 || buffer.toString("base64").replace(/=+$/, "") !== b64.replace(/=+$/, "")) {
34
+ return { error: "image must be valid base64", code: invalidCode };
35
+ }
36
+ const info = parsePngInfo(buffer);
37
+ if (info.error) return { error: "image must be a PNG image", code: pngCode };
38
+ return { b64, buffer, info };
39
+ }
40
+
41
+ function validateEditMask(imageB64, mask) {
42
+ if (mask == null) return { mask: null, maskBytes: 0 };
43
+ if (typeof mask !== "string" || mask.length === 0) {
44
+ return { error: "mask must be a PNG data URL or base64 string", code: "INVALID_EDIT_MASK" };
45
+ }
46
+ const maskCheck = decodePngDataUrl(mask, "INVALID_EDIT_MASK_BASE64", "INVALID_EDIT_MASK_PNG");
47
+ if (maskCheck.error) return maskCheck;
48
+ if (maskCheck.buffer.length > MAX_EDIT_MASK_BYTES) {
49
+ return { error: "mask is too large", code: "EDIT_MASK_TOO_LARGE" };
50
+ }
51
+ if (!hasPngAlphaChannel(maskCheck.info)) {
52
+ return { error: "mask PNG must include an alpha channel", code: "EDIT_MASK_NO_ALPHA" };
53
+ }
54
+ const imageCheck = decodePngDataUrl(imageB64, "INVALID_EDIT_IMAGE_BASE64", "INVALID_EDIT_IMAGE_PNG");
55
+ if (imageCheck.error) return imageCheck;
56
+ if (imageCheck.info.width !== maskCheck.info.width || imageCheck.info.height !== maskCheck.info.height) {
57
+ return { error: "mask dimensions must match image dimensions", code: "EDIT_MASK_DIMENSION_MISMATCH" };
58
+ }
59
+ return { mask: maskCheck.b64, maskBytes: maskCheck.buffer.length };
60
+ }
61
+
18
62
  export function registerEditRoutes(app, ctx) {
19
63
  app.post("/api/edit", async (req, res) => {
20
64
  const requestId = typeof req.body?.requestId === "string" ? req.body.requestId : req.id;
@@ -26,12 +70,15 @@ export function registerEditRoutes(app, ctx) {
26
70
  const {
27
71
  prompt,
28
72
  image: imageB64,
73
+ mask: rawMask,
29
74
  quality: rawQuality = "medium",
30
75
  size = "1024x1024",
31
76
  moderation = "low",
32
77
  provider = "oauth",
33
78
  mode: promptMode = "auto",
34
79
  model: rawModel,
80
+ reasoningEffort: rawReasoningEffort,
81
+ webSearchEnabled: rawWebSearchEnabled = true,
35
82
  } = req.body;
36
83
  const { quality, warnings: qualityWarnings } = normalizeOAuthParams({ provider, quality: rawQuality });
37
84
  const modelCheck = normalizeImageModel(ctx, rawModel);
@@ -42,6 +89,15 @@ export function registerEditRoutes(app, ctx) {
42
89
  return res.status(modelCheck.status).json({ error: modelCheck.error, code: modelCheck.code });
43
90
  }
44
91
  const imageModel = modelCheck.model;
92
+ const reasoningCheck = normalizeReasoningEffort(ctx, rawReasoningEffort);
93
+ if (reasoningCheck.error) {
94
+ finishStatus = "error";
95
+ finishHttpStatus = reasoningCheck.status;
96
+ finishErrorCode = reasoningCheck.code;
97
+ return res.status(reasoningCheck.status).json({ error: reasoningCheck.error, code: reasoningCheck.code });
98
+ }
99
+ const reasoningEffort = reasoningCheck.effort;
100
+ const webSearchEnabled = rawWebSearchEnabled !== false;
45
101
  const sessionId = typeof req.body?.sessionId === "string" ? req.body.sessionId : null;
46
102
  const normalizedPromptMode = promptMode === "direct" ? "direct" : "auto";
47
103
 
@@ -64,6 +120,13 @@ export function registerEditRoutes(app, ctx) {
64
120
  finishErrorCode = "INVALID_EDIT_INPUT";
65
121
  return res.status(400).json({ error: "Prompt and image are required" });
66
122
  }
123
+ const maskCheck = validateEditMask(imageB64, rawMask);
124
+ if (maskCheck.error) {
125
+ finishStatus = "error";
126
+ finishHttpStatus = 400;
127
+ finishErrorCode = maskCheck.code;
128
+ return res.status(400).json({ error: maskCheck.error, code: maskCheck.code });
129
+ }
67
130
  const moderationCheck = validateModeration(ctx, moderation);
68
131
  if (moderationCheck.error) {
69
132
  finishStatus = "error";
@@ -89,10 +152,13 @@ export function registerEditRoutes(app, ctx) {
89
152
  sessionId,
90
153
  promptChars: typeof prompt === "string" ? prompt.length : 0,
91
154
  promptMode: normalizedPromptMode,
155
+ webSearchEnabled,
92
156
  inputImageChars: typeof imageB64 === "string" ? imageB64.length : 0,
157
+ maskPresent: Boolean(maskCheck.mask),
158
+ maskBytes: maskCheck.maskBytes ?? 0,
93
159
  });
94
160
  const startTime = Date.now();
95
- const { b64: resultB64, usage, revisedPrompt } = await editViaOAuth(
161
+ const { b64: resultB64, usage, revisedPrompt, webSearchCalls = 0 } = await editViaOAuth(
96
162
  prompt,
97
163
  imageB64,
98
164
  quality,
@@ -101,7 +167,7 @@ export function registerEditRoutes(app, ctx) {
101
167
  normalizedPromptMode,
102
168
  ctx,
103
169
  requestId,
104
- { model: imageModel },
170
+ { model: imageModel, reasoningEffort, webSearchEnabled, mask: maskCheck.mask },
105
171
  );
106
172
 
107
173
  const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
@@ -122,7 +188,8 @@ export function registerEditRoutes(app, ctx) {
122
188
  kind: "edit",
123
189
  createdAt: Date.now(),
124
190
  usage: usage || null,
125
- webSearchCalls: 0,
191
+ webSearchCalls,
192
+ webSearchEnabled,
126
193
  };
127
194
  await writeFile(join(ctx.config.storage.generatedDir, filename + ".json"), JSON.stringify(meta)).catch(() => {});
128
195
  finishHttpStatus = 200;
@@ -145,6 +212,8 @@ export function registerEditRoutes(app, ctx) {
145
212
  warnings: qualityWarnings,
146
213
  revisedPrompt: revisedPrompt || null,
147
214
  promptMode: normalizedPromptMode,
215
+ webSearchCalls,
216
+ webSearchEnabled,
148
217
  });
149
218
  } catch (err) {
150
219
  const fallbackCode = err.code || classifyUpstreamError(err.message);
@@ -4,7 +4,7 @@ import { randomBytes } from "crypto";
4
4
  import { validateAndNormalizeRefs } from "../lib/refs.js";
5
5
  import { classifyUpstreamError } from "../lib/errorClassify.js";
6
6
  import { normalizeOAuthParams } from "../lib/oauthNormalize.js";
7
- import { normalizeImageModel } from "../lib/imageModels.js";
7
+ import { normalizeImageModel, normalizeReasoningEffort } from "../lib/imageModels.js";
8
8
  import { generateViaOAuth } from "../lib/oauthProxy.js";
9
9
  import { isNonRetryableGenerationError, normalizeGenerationFailure } from "../lib/generationErrors.js";
10
10
  import { startJob, finishJob } from "../lib/inflight.js";
@@ -39,6 +39,8 @@ export function registerGenerateRoutes(app, ctx) {
39
39
  references = [],
40
40
  mode: promptMode = "auto",
41
41
  model: rawModel,
42
+ reasoningEffort: rawReasoningEffort,
43
+ webSearchEnabled: rawWebSearchEnabled = true,
42
44
  } = req.body;
43
45
  const { quality, warnings: qualityWarnings } = normalizeOAuthParams({ provider, quality: rawQuality });
44
46
  const modelCheck = normalizeImageModel(ctx, rawModel);
@@ -49,6 +51,15 @@ export function registerGenerateRoutes(app, ctx) {
49
51
  return res.status(modelCheck.status).json({ error: modelCheck.error, code: modelCheck.code });
50
52
  }
51
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;
52
63
  const normalizedPromptMode = promptMode === "direct" ? "direct" : "auto";
53
64
 
54
65
  if (!prompt) return res.status(400).json({ error: "Prompt is required" });
@@ -106,6 +117,7 @@ export function registerGenerateRoutes(app, ctx) {
106
117
  clientNodeId,
107
118
  promptChars: typeof prompt === "string" ? prompt.length : 0,
108
119
  promptMode: normalizedPromptMode,
120
+ webSearchEnabled,
109
121
  });
110
122
  const startTime = Date.now();
111
123
 
@@ -127,7 +139,7 @@ export function registerGenerateRoutes(app, ctx) {
127
139
  requestId,
128
140
  normalizedPromptMode,
129
141
  ctx,
130
- { model: imageModel },
142
+ { model: imageModel, reasoningEffort, webSearchEnabled },
131
143
  );
132
144
  if (r.b64) return r;
133
145
  lastErr = new Error("Empty response (safety refusal)");
@@ -170,6 +182,7 @@ export function registerGenerateRoutes(app, ctx) {
170
182
  createdAt: Date.now(),
171
183
  usage: r.value.usage || null,
172
184
  webSearchCalls: r.value.webSearchCalls || 0,
185
+ webSearchEnabled,
173
186
  refsCount: refCheck.refs.length,
174
187
  };
175
188
  const rawBuffer = Buffer.from(r.value.b64, "base64");
@@ -242,6 +255,7 @@ export function registerGenerateRoutes(app, ctx) {
242
255
  warnings: qualityWarnings,
243
256
  revisedPrompt: firstRevised,
244
257
  promptMode: normalizedPromptMode,
258
+ webSearchEnabled,
245
259
  };
246
260
 
247
261
  if (count === 1) {
package/routes/index.js CHANGED
@@ -9,12 +9,19 @@ import { registerStorageRoutes } from "./storage.js";
9
9
  import { registerCardNewsRoutes } from "./cardNews.js";
10
10
  import { registerMetadataRoutes } from "./metadata.js";
11
11
  import { registerPromptRoutes } from "./prompts.js";
12
+ import { registerPromptImportRoutes } from "./promptImport.js";
13
+ import { registerAnnotationRoutes } from "./annotations.js";
14
+ import { registerCanvasVersionRoutes } from "./canvasVersions.js";
15
+ import { registerComfyRoutes } from "./comfy.js";
12
16
 
13
17
  export function configureRoutes(app, ctx) {
14
18
  registerHealthRoutes(app, ctx);
15
19
  registerStorageRoutes(app, ctx);
16
20
  registerMetadataRoutes(app, ctx);
17
21
  registerHistoryRoutes(app, ctx);
22
+ registerAnnotationRoutes(app, ctx);
23
+ registerCanvasVersionRoutes(app, ctx);
24
+ registerComfyRoutes(app, ctx);
18
25
  registerSessionRoutes(app, ctx);
19
26
  registerEditRoutes(app, ctx);
20
27
  registerNodeRoutes(app, ctx);
@@ -22,4 +29,5 @@ export function configureRoutes(app, ctx) {
22
29
  registerMultimodeRoutes(app, ctx);
23
30
  registerGenerateRoutes(app, ctx);
24
31
  registerPromptRoutes(app, ctx);
32
+ registerPromptImportRoutes(app, ctx);
25
33
  }
@@ -4,7 +4,7 @@ import { randomBytes } from "crypto";
4
4
  import { validateAndNormalizeRefs } from "../lib/refs.js";
5
5
  import { classifyUpstreamError } from "../lib/errorClassify.js";
6
6
  import { normalizeOAuthParams } from "../lib/oauthNormalize.js";
7
- import { normalizeImageModel } from "../lib/imageModels.js";
7
+ import { normalizeImageModel, normalizeReasoningEffort } from "../lib/imageModels.js";
8
8
  import { generateMultimodeViaOAuth } from "../lib/oauthProxy.js";
9
9
  import { startJob, finishJob } from "../lib/inflight.js";
10
10
  import { logEvent, logError } from "../lib/logger.js";
@@ -56,6 +56,8 @@ export function registerMultimodeRoutes(app, ctx) {
56
56
  references = [],
57
57
  mode: promptMode = "auto",
58
58
  model: rawModel,
59
+ reasoningEffort: rawReasoningEffort,
60
+ webSearchEnabled: rawWebSearchEnabled = true,
59
61
  } = req.body;
60
62
  const maxImages = normalizeMaxImages(req.body?.maxImages);
61
63
  const normalizedPromptMode = promptMode === "direct" ? "direct" : "auto";
@@ -69,6 +71,16 @@ export function registerMultimodeRoutes(app, ctx) {
69
71
  return;
70
72
  }
71
73
  const imageModel = modelCheck.model;
74
+ const reasoningCheck = normalizeReasoningEffort(ctx, rawReasoningEffort);
75
+ if (reasoningCheck.error) {
76
+ finishStatus = "error";
77
+ finishHttpStatus = reasoningCheck.status;
78
+ finishErrorCode = reasoningCheck.code;
79
+ sendSse(res, "error", { error: reasoningCheck.error, code: reasoningCheck.code, status: reasoningCheck.status, requestId });
80
+ return;
81
+ }
82
+ const reasoningEffort = reasoningCheck.effort;
83
+ const webSearchEnabled = rawWebSearchEnabled !== false;
72
84
  if (!prompt) {
73
85
  finishStatus = "error";
74
86
  finishHttpStatus = 400;
@@ -122,6 +134,7 @@ export function registerMultimodeRoutes(app, ctx) {
122
134
  maxImages,
123
135
  refs: refCheck.refs.length,
124
136
  promptChars: typeof prompt === "string" ? prompt.length : 0,
137
+ webSearchEnabled,
125
138
  });
126
139
 
127
140
  const startTime = Date.now();
@@ -143,6 +156,8 @@ export function registerMultimodeRoutes(app, ctx) {
143
156
  {
144
157
  model: imageModel,
145
158
  maxImages,
159
+ reasoningEffort,
160
+ webSearchEnabled,
146
161
  onPartialImage: (partial) =>
147
162
  sendSse(res, "partial", {
148
163
  image: `data:${mime};base64,${partial.b64}`,
@@ -184,6 +199,7 @@ export function registerMultimodeRoutes(app, ctx) {
184
199
  createdAt: Date.now(),
185
200
  usage: generated.usage || null,
186
201
  webSearchCalls: generated.webSearchCalls || 0,
202
+ webSearchEnabled,
187
203
  refsCount: refCheck.refs.length,
188
204
  };
189
205
  const rawBuffer = Buffer.from(image.b64, "base64");
@@ -224,6 +240,7 @@ export function registerMultimodeRoutes(app, ctx) {
224
240
  model: imageModel,
225
241
  usage: generated.usage || null,
226
242
  webSearchCalls: generated.webSearchCalls || 0,
243
+ webSearchEnabled,
227
244
  warnings: qualityWarnings,
228
245
  extraIgnored: generated.extraIgnored || 0,
229
246
  promptMode: normalizedPromptMode,
package/routes/nodes.js CHANGED
@@ -10,7 +10,7 @@ import { startJob, finishJob } from "../lib/inflight.js";
10
10
  import { validateAndNormalizeRefs } from "../lib/refs.js";
11
11
  import { classifyUpstreamError } from "../lib/errorClassify.js";
12
12
  import { normalizeOAuthParams } from "../lib/oauthNormalize.js";
13
- import { normalizeImageModel } from "../lib/imageModels.js";
13
+ import { normalizeImageModel, normalizeReasoningEffort } from "../lib/imageModels.js";
14
14
  import { generateViaOAuth, editViaOAuth } from "../lib/oauthProxy.js";
15
15
  import { isNonRetryableGenerationError, normalizeGenerationFailure } from "../lib/generationErrors.js";
16
16
  import { logEvent, logError } from "../lib/logger.js";
@@ -87,6 +87,7 @@ export function registerNodeRoutes(app, ctx) {
87
87
  contextMode: rawContextMode = "parent-plus-refs",
88
88
  searchMode: rawSearchMode = "on",
89
89
  model: rawModel,
90
+ reasoningEffort: rawReasoningEffort,
90
91
  } = body;
91
92
  const { provider = "oauth" } = body;
92
93
  const { quality, warnings: qualityWarnings } = normalizeOAuthParams({ provider, quality: rawQuality });
@@ -101,11 +102,23 @@ export function registerNodeRoutes(app, ctx) {
101
102
  });
102
103
  }
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;
104
116
  const normalizedPromptMode = promptMode === "direct" ? "direct" : "auto";
105
117
  const contextMode = ["parent-plus-refs", "parent-only", "ancestry"].includes(rawContextMode)
106
118
  ? rawContextMode
107
119
  : "parent-plus-refs";
108
120
  const searchMode = ["off", "auto", "on"].includes(rawSearchMode) ? rawSearchMode : "on";
121
+ const webSearchEnabled = body.webSearchEnabled !== false && searchMode !== "off";
109
122
  if (contextMode === "ancestry") {
110
123
  finishStatus = "error";
111
124
  finishHttpStatus = 400;
@@ -168,7 +181,6 @@ export function registerNodeRoutes(app, ctx) {
168
181
  const generateReferenceDiagnostics = operation === "generate" ? referenceDiagnostics : [];
169
182
  const referenceMismatchCount = generateReferenceDiagnostics.filter((ref) => ref.warnings?.includes("mime_mismatch")).length;
170
183
  const refsForRequest = contextMode === "parent-only" ? [] : (refCheck.refDetails || refCheck.refs);
171
- const webSearchEnabled = true;
172
184
  const parentImagePresent = !!parentB64;
173
185
  const inputImageCount = (parentImagePresent ? 1 : 0) + refsForRequest.length;
174
186
  logEvent("node", "request", {
@@ -227,7 +239,13 @@ export function registerNodeRoutes(app, ctx) {
227
239
  webSearchEnabled,
228
240
  });
229
241
  const r = parentB64
230
- ? await editViaOAuth(prompt, parentB64, quality, size, moderation, normalizedPromptMode, ctx, requestId, { model: imageModel, references: refsForRequest, searchMode: "on" })
242
+ ? await editViaOAuth(prompt, parentB64, quality, size, moderation, normalizedPromptMode, ctx, requestId, {
243
+ model: imageModel,
244
+ references: refsForRequest,
245
+ searchMode,
246
+ reasoningEffort,
247
+ webSearchEnabled,
248
+ })
231
249
  : await generateViaOAuth(
232
250
  prompt,
233
251
  quality,
@@ -239,6 +257,8 @@ export function registerNodeRoutes(app, ctx) {
239
257
  ctx,
240
258
  {
241
259
  model: imageModel,
260
+ reasoningEffort,
261
+ webSearchEnabled,
242
262
  partialImages: streamResponse ? 2 : 0,
243
263
  onPartialImage: streamResponse
244
264
  ? (partial) =>
@@ -336,6 +356,7 @@ export function registerNodeRoutes(app, ctx) {
336
356
  elapsed,
337
357
  usage: usage || null,
338
358
  webSearchCalls,
359
+ webSearchEnabled,
339
360
  contextMode,
340
361
  searchMode,
341
362
  provider: "oauth",
@@ -375,6 +396,7 @@ export function registerNodeRoutes(app, ctx) {
375
396
  elapsed,
376
397
  usage,
377
398
  webSearchCalls,
399
+ webSearchEnabled,
378
400
  provider: "oauth",
379
401
  model: imageModel,
380
402
  size,