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.
- package/.env.example +5 -0
- package/README.md +3 -0
- package/config.js +58 -0
- package/docs/FAQ.ko.md +20 -0
- package/docs/FAQ.md +20 -0
- package/docs/README.ko.md +3 -0
- package/docs/README.zh-CN.md +3 -0
- package/integrations/comfyui/ima2_gen_bridge/README.md +88 -0
- package/integrations/comfyui/ima2_gen_bridge/__init__.py +3 -0
- package/integrations/comfyui/ima2_gen_bridge/__pycache__/__init__.cpython-313.pyc +0 -0
- package/integrations/comfyui/ima2_gen_bridge/__pycache__/nodes.cpython-313.pyc +0 -0
- package/integrations/comfyui/ima2_gen_bridge/nodes.py +238 -0
- package/lib/canvasVersionStore.js +181 -0
- package/lib/cardNewsPlannerClient.js +4 -2
- package/lib/comfyBridge.js +214 -0
- package/lib/db.js +14 -0
- package/lib/historyList.js +4 -0
- package/lib/imageMetadata.js +4 -0
- package/lib/imageModels.js +20 -0
- package/lib/oauthProxy.js +88 -38
- package/lib/pngInfo.js +26 -0
- package/lib/promptImport/errors.js +16 -0
- package/lib/promptImport/githubSource.js +205 -0
- package/lib/promptImport/parsePromptCandidates.js +140 -0
- package/package.json +3 -2
- package/routes/annotations.js +95 -0
- package/routes/canvasVersions.js +64 -0
- package/routes/comfy.js +39 -0
- package/routes/edit.js +73 -4
- package/routes/generate.js +16 -2
- package/routes/index.js +8 -0
- package/routes/multimode.js +18 -1
- package/routes/nodes.js +25 -3
- package/routes/promptImport.js +175 -0
- package/ui/dist/assets/index-DARPdT4Q.css +1 -0
- package/ui/dist/assets/index-ht80GMq4.js +31 -0
- package/ui/dist/assets/index-ht80GMq4.js.map +1 -0
- package/ui/dist/index.html +2 -2
- package/ui/dist/assets/index-3X-6VjbF.css +0 -1
- package/ui/dist/assets/index-DPSq9qEs.js +0 -31
- 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.
|
|
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
|
+
}
|
package/routes/comfy.js
ADDED
|
@@ -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
|
|
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);
|
package/routes/generate.js
CHANGED
|
@@ -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
|
}
|
package/routes/multimode.js
CHANGED
|
@@ -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, {
|
|
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,
|