ima2-gen 2.0.1 → 2.0.3
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/CHANGELOG.md +151 -0
- package/README.md +10 -1
- package/bin/commands/backfillThumbs.js +6 -0
- package/bin/commands/gen.js +13 -5
- package/bin/commands/multimode.js +7 -5
- package/bin/commands/node.js +2 -1
- package/bin/ima2.js +14 -10
- package/config.js +8 -1
- package/docs/API.md +133 -9
- package/docs/CLI.md +6 -5
- package/docs/FAQ.ko.md +16 -0
- package/docs/FAQ.md +30 -0
- package/docs/README.ko.md +7 -3
- package/docs/migration/runtime-test-inventory.md +25 -1
- package/lib/agentCommandParser.js +10 -6
- package/lib/agentGenerationPlanner.js +96 -19
- package/lib/agentImageVideoGen.js +281 -0
- package/lib/agentPlannerModel.js +172 -0
- package/lib/agentQuestionResponder.js +9 -3
- package/lib/agentQueueStore.js +42 -0
- package/lib/agentQueueWorker.js +57 -7
- package/lib/agentRuntime.js +103 -274
- package/lib/agentSettings.js +9 -6
- package/lib/agentToolManifest.js +90 -0
- package/lib/agentTypes.js +1 -0
- package/lib/agyCli.js +31 -0
- package/lib/agyImageAdapter.js +104 -12
- package/lib/capabilities.js +7 -5
- package/lib/configKeys.js +6 -0
- package/lib/errorClassify.js +8 -7
- package/lib/eventBus.js +71 -0
- package/lib/geminiApiImageAdapter.js +24 -21
- package/lib/generationErrors.js +3 -1
- package/lib/generationRequestLog.js +25 -0
- package/lib/grokImageAdapter.js +76 -144
- package/lib/grokImageCore.js +153 -0
- package/lib/grokMultimodeAdapter.js +7 -4
- package/lib/grokVideoAdapter.js +3 -2
- package/lib/grokVideoCanvas.js +13 -0
- package/lib/grokVideoPlannerPrompt.js +52 -12
- package/lib/historyList.js +1 -0
- package/lib/inflight.js +54 -17
- package/lib/multimodeHelpers.js +11 -0
- package/lib/nodeHelpers.js +59 -0
- package/lib/oauthProxy/generators.js +6 -5
- package/lib/oauthProxy/prompts.js +60 -41
- package/lib/oauthProxy/streams.js +1 -1
- package/lib/promptBuilder/systemPrompt.js +2 -5
- package/lib/promptSafetyPolicy.js +1 -5
- package/lib/responsesDoctor.js +3 -3
- package/lib/responsesFallback.js +29 -15
- package/lib/responsesImageAdapter.js +11 -7
- package/lib/routeHelpers.js +44 -0
- package/lib/ssePublish.js +12 -0
- package/lib/storyboardPrefix.js +28 -0
- package/lib/thumbBackfill.js +16 -5
- package/node_modules/qs/CHANGELOG.md +10 -0
- package/node_modules/qs/README.md +1 -1
- package/node_modules/qs/dist/qs.js +15 -15
- package/node_modules/qs/eslint.config.mjs +1 -0
- package/node_modules/qs/lib/parse.js +52 -22
- package/node_modules/qs/lib/stringify.js +11 -4
- package/node_modules/qs/package.json +2 -2
- package/node_modules/qs/test/parse.js +49 -0
- package/node_modules/qs/test/stringify.js +129 -0
- package/package.json +4 -1
- package/routes/agent.js +19 -2
- package/routes/agy.js +48 -0
- package/routes/auth.js +6 -2
- package/routes/edit.js +7 -1
- package/routes/events.js +78 -0
- package/routes/generate.js +111 -128
- package/routes/generationRequestLog.js +16 -0
- package/routes/index.js +6 -0
- package/routes/keys.js +22 -2
- package/routes/multimode.js +100 -57
- package/routes/nodes.js +64 -108
- package/routes/video.js +100 -17
- package/skills/ima2/SKILL.md +98 -21
- package/ui/dist/.vite/manifest.json +22 -12
- package/ui/dist/assets/AgentWorkspace-qEMrATBP.js +3 -0
- package/ui/dist/assets/{CardNewsWorkspace-Dqyc1WZ1.js → CardNewsWorkspace-DoJnVHmn.js} +1 -1
- package/ui/dist/assets/GenerationRequestLogPanel-BDWRRLNV.js +1 -0
- package/ui/dist/assets/{NodeCanvas-ChEXzQbb.js → NodeCanvas-DIMo45sp.js} +3 -3
- package/ui/dist/assets/{PromptBuilderPanel-B95ZufnR.js → PromptBuilderPanel-BF3lcSJD.js} +2 -2
- package/ui/dist/assets/{PromptImportDialog-DGOwFQET.js → PromptImportDialog-Cy6ZymKG.js} +2 -2
- package/ui/dist/assets/{PromptImportDiscoverySection-CgvdnR49.js → PromptImportDiscoverySection-CzuW-8P2.js} +1 -1
- package/ui/dist/assets/PromptImportFolderSection-DP5ywnsv.js +1 -0
- package/ui/dist/assets/{PromptLibraryPanel-B9kndPw1.js → PromptLibraryPanel-CAsKr7CV.js} +2 -2
- package/ui/dist/assets/SettingsWorkspace-_PdPVsxi.js +1 -0
- package/ui/dist/assets/index-BUinlX2n.js +4 -0
- package/ui/dist/assets/index-CX3fge8X.css +1 -0
- package/ui/dist/assets/index-ygo6nfqx.js +23 -0
- package/ui/dist/index.html +2 -2
- package/ui/dist/assets/AgentWorkspace-CYv84Rus.js +0 -3
- package/ui/dist/assets/PromptImportFolderSection-CfUye9J8.js +0 -1
- package/ui/dist/assets/SettingsWorkspace-B3tgLrmF.js +0 -1
- package/ui/dist/assets/index-BhcvL0g-.js +0 -1
- package/ui/dist/assets/index-BtK3YhJc.js +0 -39
- package/ui/dist/assets/index-ClOLOjnA.css +0 -1
package/routes/generate.js
CHANGED
|
@@ -5,6 +5,7 @@ import { randomBytes } from "crypto";
|
|
|
5
5
|
import { detectImageMimeFromB64, summarizeReferencePayload, validateAndNormalizeRefs } from "../lib/refs.js";
|
|
6
6
|
import { generateImageThumbnailFromBuffer } from "../lib/imageThumb.js";
|
|
7
7
|
import { classifyUpstreamError } from "../lib/errorClassify.js";
|
|
8
|
+
import { appendGenerationRequestLog } from "../lib/generationRequestLog.js";
|
|
8
9
|
import { normalizeOAuthParams } from "../lib/oauthNormalize.js";
|
|
9
10
|
import { resolveProviderOptions } from "../lib/providerOptions.js";
|
|
10
11
|
import { generateViaResponses } from "../lib/responsesImageAdapter.js";
|
|
@@ -12,7 +13,7 @@ import { generateViaGrok, planGrokImage } from "../lib/grokImageAdapter.js";
|
|
|
12
13
|
import { generateViaAgy } from "../lib/agyImageAdapter.js";
|
|
13
14
|
import { generateViaGeminiApi } from "../lib/geminiApiImageAdapter.js";
|
|
14
15
|
import { isNonRetryableGenerationError, normalizeGenerationFailure } from "../lib/generationErrors.js";
|
|
15
|
-
import { startJob, finishJob, registerJobAbortController, isJobCanceled } from "../lib/inflight.js";
|
|
16
|
+
import { startJob, finishJob, registerJobAbortController, isJobCanceled, isStartJobFailure, setJobPhase, INFLIGHT_RETRY_AFTER_SECONDS, } from "../lib/inflight.js";
|
|
16
17
|
import { isGenerationCanceledError, makeGenerationCanceledError, throwIfJobCanceled, } from "../lib/generationCancel.js";
|
|
17
18
|
import { logEvent, logError } from "../lib/logger.js";
|
|
18
19
|
import { embedImageMetadataBestEffort } from "../lib/imageMetadataStore.js";
|
|
@@ -20,57 +21,44 @@ import { invalidateHistoryIndex } from "../lib/historyIndex.js";
|
|
|
20
21
|
import { normalizeComposerInsertedPrompts, normalizeComposerPrompt, } from "../lib/composerSnapshot.js";
|
|
21
22
|
import { errInfo } from "../lib/errInfo.js";
|
|
22
23
|
import { requireRuntimeContext } from "../lib/runtimeContext.js";
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
return { moderation };
|
|
28
|
-
}
|
|
29
|
-
function imageFormatFromMime(mime) {
|
|
30
|
-
if (mime === "image/jpeg")
|
|
31
|
-
return "jpeg";
|
|
32
|
-
if (mime === "image/webp")
|
|
33
|
-
return "webp";
|
|
34
|
-
return "png";
|
|
35
|
-
}
|
|
24
|
+
import { STORYBOARD_PREFIX } from "../lib/storyboardPrefix.js";
|
|
25
|
+
import { validateModeration, imageFormatFromMime, upstreamErrorFields } from "../lib/routeHelpers.js";
|
|
26
|
+
import { publish } from "../lib/eventBus.js";
|
|
27
|
+
import { publishJobEvent } from "../lib/ssePublish.js";
|
|
36
28
|
export function registerGenerateRoutes(app, ctxRaw) {
|
|
37
29
|
const ctx = requireRuntimeContext(ctxRaw);
|
|
38
30
|
app.post("/api/generate", async (req, res) => {
|
|
39
31
|
const requestId = typeof req.body?.requestId === "string" ? req.body.requestId : req.id;
|
|
32
|
+
const asyncMode = req.body?.async === true;
|
|
40
33
|
let finishStatus = "completed";
|
|
41
34
|
let finishHttpStatus;
|
|
42
35
|
let finishErrorCode;
|
|
43
36
|
let finishMeta = {};
|
|
44
37
|
let finishCanceled = false;
|
|
45
38
|
const cancelController = new AbortController();
|
|
39
|
+
const fail = (status, payload) => {
|
|
40
|
+
finishStatus = "error";
|
|
41
|
+
finishHttpStatus = status;
|
|
42
|
+
finishErrorCode = typeof payload.code === "string" ? payload.code : finishErrorCode;
|
|
43
|
+
if (asyncMode && res.headersSent) {
|
|
44
|
+
publish(requestId, "error", { ...payload, status, requestId });
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
return res.status(status).json(payload);
|
|
48
|
+
};
|
|
49
|
+
const succeed = (payload) => {
|
|
50
|
+
if (asyncMode) {
|
|
51
|
+
publishJobEvent(requestId, "done", payload);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
res.json(payload);
|
|
55
|
+
};
|
|
46
56
|
try {
|
|
47
57
|
const sessionId = typeof req.body?.sessionId === "string" ? req.body.sessionId : null;
|
|
48
58
|
const clientNodeId = typeof req.body?.clientNodeId === "string" ? req.body.clientNodeId : null;
|
|
49
59
|
const { prompt, quality: rawQuality = "medium", size = "1024x1024", format = "png", moderation = "low", provider = "auto", n = 1, references = [], mode: promptMode = "auto", model: rawModel, reasoningEffort: rawReasoningEffort, webSearchEnabled: rawWebSearchEnabled = true, } = req.body;
|
|
50
60
|
const storyboardActive = req.body?.storyboard === true;
|
|
51
|
-
const storyboardPrefix = storyboardActive
|
|
52
|
-
? [
|
|
53
|
-
"[STORYBOARD MODE — Video Production Keyframe]",
|
|
54
|
-
"This image is a keyframe for a multi-shot VIDEO storyboard. It will be animated via image-to-video.",
|
|
55
|
-
"The prompt and all injected instructions MUST be in English.",
|
|
56
|
-
"",
|
|
57
|
-
"CHARACTER LOCK:",
|
|
58
|
-
"- Identify each character by 2-3 VISUAL identifiers (clothing color + physique + position/props). Never by name alone.",
|
|
59
|
-
"- Copy character descriptions VERBATIM from the reference/prior frame. Do NOT rephrase or drift.",
|
|
60
|
-
"",
|
|
61
|
-
"SCENE CONTINUITY:",
|
|
62
|
-
"- Lock lighting direction, color palette, environment, and art style to prior frames.",
|
|
63
|
-
"- Change ONLY: action, shot scale, camera angle, or expression.",
|
|
64
|
-
"- Reference image = canonical anchor. Preserve it faithfully.",
|
|
65
|
-
"",
|
|
66
|
-
"VIDEO-READY COMPOSITION:",
|
|
67
|
-
"- Frame for animation: leave space for motion, avoid static-only poses.",
|
|
68
|
-
"- Use descriptive caption format: shot type + subject action + environment + technical (lens, lighting) + mood.",
|
|
69
|
-
"- Specify intended camera movement for the video phase (e.g. 'slow dolly-in', 'static wide').",
|
|
70
|
-
"- End pose must be stable and suitable for video continuation.",
|
|
71
|
-
"",
|
|
72
|
-
].join("\n") + "\n"
|
|
73
|
-
: "";
|
|
61
|
+
const storyboardPrefix = storyboardActive ? STORYBOARD_PREFIX : "";
|
|
74
62
|
const composerPrompt = normalizeComposerPrompt(req.body?.composerPrompt);
|
|
75
63
|
const composerInsertedPrompts = normalizeComposerInsertedPrompts(req.body?.composerInsertedPrompts);
|
|
76
64
|
const { quality, warnings: qualityWarnings } = normalizeOAuthParams({ provider, quality: rawQuality });
|
|
@@ -82,10 +70,7 @@ export function registerGenerateRoutes(app, ctxRaw) {
|
|
|
82
70
|
rawWebSearchEnabled,
|
|
83
71
|
});
|
|
84
72
|
if (providerOptions.error) {
|
|
85
|
-
|
|
86
|
-
finishHttpStatus = providerOptions.status;
|
|
87
|
-
finishErrorCode = providerOptions.code;
|
|
88
|
-
return res.status(providerOptions.status).json({ error: providerOptions.error, code: providerOptions.code });
|
|
73
|
+
return fail(providerOptions.status, { error: providerOptions.error, code: providerOptions.code });
|
|
89
74
|
}
|
|
90
75
|
const imageModel = providerOptions.model;
|
|
91
76
|
const reasoningEffort = providerOptions.reasoningEffort;
|
|
@@ -95,13 +80,35 @@ export function registerGenerateRoutes(app, ctxRaw) {
|
|
|
95
80
|
const normalizedPromptMode = promptMode === "direct" ? "direct" : "auto";
|
|
96
81
|
const generationPrompt = storyboardPrefix + prompt;
|
|
97
82
|
if (!prompt)
|
|
98
|
-
return
|
|
83
|
+
return fail(400, { error: "Prompt is required" });
|
|
99
84
|
const moderationCheck = validateModeration(ctx, moderation);
|
|
100
85
|
if (moderationCheck.error)
|
|
101
|
-
return
|
|
102
|
-
const
|
|
86
|
+
return fail(400, { error: moderationCheck.error });
|
|
87
|
+
const maxGeneratedImages = Math.max(1, Math.trunc(Number(ctx.config.limits.maxGeneratedImages) || 1));
|
|
88
|
+
const count = Math.min(Math.max(parseInt(n) || 1, 1), maxGeneratedImages);
|
|
103
89
|
const referencePayload = summarizeReferencePayload(references);
|
|
104
|
-
|
|
90
|
+
const refCheckResult = validateAndNormalizeRefs(references);
|
|
91
|
+
if (refCheckResult.error) {
|
|
92
|
+
return fail(400, { error: refCheckResult.error, code: refCheckResult.code });
|
|
93
|
+
}
|
|
94
|
+
const refCheck = refCheckResult;
|
|
95
|
+
const incomingProviderUrl = typeof req.body?.providerUrl === "string" && req.body.providerUrl.startsWith("http")
|
|
96
|
+
? req.body.providerUrl
|
|
97
|
+
: null;
|
|
98
|
+
const grokRefs = incomingProviderUrl
|
|
99
|
+
? [{ b64: "", url: incomingProviderUrl, declaredMime: "image/png", detectedMime: "image/png" }, ...refCheck.refDetails]
|
|
100
|
+
: refCheck.refDetails;
|
|
101
|
+
const providerRefCount = activeProvider === "grok" || activeProvider === "grok-api"
|
|
102
|
+
? grokRefs.length
|
|
103
|
+
: refCheck.refs.length;
|
|
104
|
+
if ((activeProvider === "grok" || activeProvider === "agy" || activeProvider === "grok-api" || activeProvider === "gemini-api") && providerRefCount > 3) {
|
|
105
|
+
return fail(400, {
|
|
106
|
+
error: `${activeProvider === "agy" ? "Agy" : "Grok"} image editing supports up to 3 reference images`,
|
|
107
|
+
code: activeProvider === "agy" ? "AGY_REF_TOO_MANY" : "GROK_REF_TOO_MANY",
|
|
108
|
+
requestId,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
const started = startJob({
|
|
105
112
|
requestId,
|
|
106
113
|
kind: "classic",
|
|
107
114
|
prompt,
|
|
@@ -114,32 +121,33 @@ export function registerGenerateRoutes(app, ctxRaw) {
|
|
|
114
121
|
model: imageModel,
|
|
115
122
|
size: effectiveSize,
|
|
116
123
|
n: count,
|
|
117
|
-
refsCount:
|
|
124
|
+
refsCount: providerRefCount,
|
|
118
125
|
referenceBytes: referencePayload.referenceBytes,
|
|
119
126
|
referenceB64Chars: referencePayload.referenceB64Chars,
|
|
120
127
|
composerPrompt,
|
|
121
128
|
composerInsertedPrompts,
|
|
122
129
|
},
|
|
123
130
|
});
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
finishStatus = "error";
|
|
135
|
-
finishHttpStatus = 400;
|
|
136
|
-
finishErrorCode = activeProvider === "agy" ? "AGY_REF_TOO_MANY" : "GROK_REF_TOO_MANY";
|
|
137
|
-
return res.status(400).json({
|
|
138
|
-
error: `${activeProvider === "agy" ? "Agy" : "Grok"} image editing supports up to 3 reference images`,
|
|
139
|
-
code: activeProvider === "agy" ? "AGY_REF_TOO_MANY" : "GROK_REF_TOO_MANY",
|
|
131
|
+
if (started && isStartJobFailure(started)) {
|
|
132
|
+
const status = started.code === "TOO_MANY_JOBS" ? 429 : 409;
|
|
133
|
+
if (started.code === "TOO_MANY_JOBS") {
|
|
134
|
+
res.setHeader("Retry-After", String(INFLIGHT_RETRY_AFTER_SECONDS));
|
|
135
|
+
}
|
|
136
|
+
return fail(status, {
|
|
137
|
+
error: started.code === "TOO_MANY_JOBS"
|
|
138
|
+
? "Too many concurrent generation jobs"
|
|
139
|
+
: "Request ID already in use",
|
|
140
|
+
code: started.code,
|
|
140
141
|
requestId,
|
|
141
142
|
});
|
|
142
143
|
}
|
|
144
|
+
registerJobAbortController(requestId, cancelController);
|
|
145
|
+
if (asyncMode) {
|
|
146
|
+
res.status(202).json({ requestId, async: true });
|
|
147
|
+
}
|
|
148
|
+
setJobPhase(requestId, "streaming");
|
|
149
|
+
if (asyncMode)
|
|
150
|
+
publish(requestId, "phase", { requestId, phase: "streaming" });
|
|
143
151
|
const client = req.get("x-ima2-client") || "ui";
|
|
144
152
|
const referenceDiagnostics = refCheck.referenceDiagnostics || [];
|
|
145
153
|
const referenceMismatchCount = referenceDiagnostics.filter((ref) => ref.warnings?.includes("mime_mismatch")).length;
|
|
@@ -152,7 +160,7 @@ export function registerGenerateRoutes(app, ctxRaw) {
|
|
|
152
160
|
size: effectiveSize,
|
|
153
161
|
moderation,
|
|
154
162
|
n: count,
|
|
155
|
-
refs:
|
|
163
|
+
refs: providerRefCount,
|
|
156
164
|
referenceBytes: referencePayload.referenceBytes,
|
|
157
165
|
referenceMismatchCount,
|
|
158
166
|
refDetectedMimes: [...new Set(referenceDiagnostics.map((ref) => ref.detectedMime).filter(Boolean))].join(","),
|
|
@@ -175,8 +183,8 @@ export function registerGenerateRoutes(app, ctxRaw) {
|
|
|
175
183
|
size: effectiveSize,
|
|
176
184
|
signal: cancelController.signal,
|
|
177
185
|
requestId,
|
|
178
|
-
referenceCount:
|
|
179
|
-
references:
|
|
186
|
+
referenceCount: grokRefs.length,
|
|
187
|
+
references: grokRefs,
|
|
180
188
|
directApiKey: grokDirectApiKey,
|
|
181
189
|
})
|
|
182
190
|
: null;
|
|
@@ -210,7 +218,7 @@ export function registerGenerateRoutes(app, ctxRaw) {
|
|
|
210
218
|
requestId,
|
|
211
219
|
plannedPrompt: sharedGrokPlan?.prompt,
|
|
212
220
|
webSearchCalls: sharedGrokPlan?.webSearchCalls,
|
|
213
|
-
references:
|
|
221
|
+
references: grokRefs,
|
|
214
222
|
directApiKey: grokDirectApiKey,
|
|
215
223
|
});
|
|
216
224
|
throwIfJobCanceled(requestId);
|
|
@@ -275,6 +283,11 @@ export function registerGenerateRoutes(app, ctxRaw) {
|
|
|
275
283
|
}
|
|
276
284
|
const rand = randomBytes(ctx.config.ids.generatedHexBytes).toString("hex");
|
|
277
285
|
const filename = `${Date.now()}_${rand}_${images.length}.${resultFormat}`;
|
|
286
|
+
const createdAt = Date.now();
|
|
287
|
+
const valueWithProviderUrl = r.value;
|
|
288
|
+
const providerUrl = typeof valueWithProviderUrl.providerUrl === "string"
|
|
289
|
+
? valueWithProviderUrl.providerUrl
|
|
290
|
+
: undefined;
|
|
278
291
|
const meta = {
|
|
279
292
|
kind: "classic",
|
|
280
293
|
requestId,
|
|
@@ -293,11 +306,12 @@ export function registerGenerateRoutes(app, ctxRaw) {
|
|
|
293
306
|
model: activeProvider === "grok" ? (quality === "high" ? "grok-imagine-image-quality" : imageModel) : imageModel,
|
|
294
307
|
reasoningEffort,
|
|
295
308
|
provider: activeProvider,
|
|
296
|
-
createdAt
|
|
309
|
+
createdAt,
|
|
310
|
+
...(providerUrl ? { providerUrl } : {}),
|
|
297
311
|
usage: r.value.usage || null,
|
|
298
312
|
webSearchCalls: r.value.webSearchCalls || 0,
|
|
299
313
|
webSearchEnabled,
|
|
300
|
-
refsCount:
|
|
314
|
+
refsCount: providerRefCount,
|
|
301
315
|
};
|
|
302
316
|
const rawBuffer = Buffer.from(r.value.b64, "base64");
|
|
303
317
|
const embedded = await embedImageMetadataBestEffort(rawBuffer, resultFormat, meta, {
|
|
@@ -320,6 +334,8 @@ export function registerGenerateRoutes(app, ctxRaw) {
|
|
|
320
334
|
image: `data:${resultMime};base64,${r.value.b64}`,
|
|
321
335
|
filename,
|
|
322
336
|
revisedPrompt: r.value.revisedPrompt || null,
|
|
337
|
+
...(providerUrl ? { providerUrl } : {}),
|
|
338
|
+
createdAt,
|
|
323
339
|
});
|
|
324
340
|
if (r.value.usage) {
|
|
325
341
|
const usageValue = r.value.usage;
|
|
@@ -349,47 +365,20 @@ export function registerGenerateRoutes(app, ctxRaw) {
|
|
|
349
365
|
const status = firstErr.status || 500;
|
|
350
366
|
if (isGenerationCanceledError(firstErr)) {
|
|
351
367
|
finishCanceled = true;
|
|
352
|
-
|
|
353
|
-
finishErrorCode = firstErr.code;
|
|
354
|
-
return res.status(firstErr.status).json({
|
|
368
|
+
return fail(firstErr.status, {
|
|
355
369
|
error: firstErr.message,
|
|
356
370
|
code: firstErr.code,
|
|
357
371
|
requestId,
|
|
358
372
|
});
|
|
359
373
|
}
|
|
360
|
-
|
|
361
|
-
finishHttpStatus = status;
|
|
362
|
-
finishErrorCode = firstErr.code;
|
|
363
|
-
return res.status(status).json({
|
|
374
|
+
return fail(status, {
|
|
364
375
|
error: firstErr.message,
|
|
365
376
|
code: firstErr.code,
|
|
366
|
-
|
|
367
|
-
upstreamType: firstErr.upstreamType || null,
|
|
368
|
-
upstreamParam: firstErr.upstreamParam || null,
|
|
369
|
-
diagnosticReason: firstErr.diagnosticReason || null,
|
|
370
|
-
retryKind: firstErr.retryKind || null,
|
|
371
|
-
initialEventCount: firstErr.initialEventCount ?? null,
|
|
372
|
-
initialEventTypes: firstErr.initialEventTypes || null,
|
|
373
|
-
referencesDroppedOnRetry: firstErr.referencesDroppedOnRetry ?? null,
|
|
374
|
-
developerPromptDroppedOnRetry: firstErr.developerPromptDroppedOnRetry ?? null,
|
|
375
|
-
webSearchDroppedOnRetry: firstErr.webSearchDroppedOnRetry ?? null,
|
|
376
|
-
fallbackEventCount: firstErr.fallbackEventCount ?? null,
|
|
377
|
-
fallbackEventTypes: firstErr.fallbackEventTypes || null,
|
|
378
|
-
fallbackImageCallSeen: firstErr.fallbackImageCallSeen ?? null,
|
|
379
|
-
fallbackImageResultCount: firstErr.fallbackImageResultCount ?? null,
|
|
380
|
-
errorEventCount: firstErr.eventCount ?? null,
|
|
381
|
-
eventTypes: firstErr.eventTypes || null,
|
|
382
|
-
webSearchCalls: firstErr.webSearchCalls ?? null,
|
|
383
|
-
responseDiagnostics: firstErr.responseDiagnostics || null,
|
|
384
|
-
toolTypes: firstErr.toolTypes || null,
|
|
385
|
-
toolChoiceKind: firstErr.toolChoiceKind || null,
|
|
377
|
+
...upstreamErrorFields(firstErr),
|
|
386
378
|
requestId,
|
|
387
379
|
});
|
|
388
380
|
}
|
|
389
|
-
|
|
390
|
-
finishHttpStatus = 500;
|
|
391
|
-
finishErrorCode = "GENERATE_ALL_FAILED";
|
|
392
|
-
return res.status(500).json({ error: "All generation attempts failed" });
|
|
381
|
+
return fail(500, { error: "All generation attempts failed", code: "GENERATE_ALL_FAILED", requestId });
|
|
393
382
|
}
|
|
394
383
|
const elapsed = +((Date.now() - startTime) / 1000).toFixed(1);
|
|
395
384
|
// Persist elapsed (computed after the generation loop) into each image's sidecar.
|
|
@@ -431,7 +420,15 @@ export function registerGenerateRoutes(app, ctxRaw) {
|
|
|
431
420
|
elapsedMs: Date.now() - startTime,
|
|
432
421
|
filename: images[0].filename,
|
|
433
422
|
});
|
|
434
|
-
|
|
423
|
+
succeed({
|
|
424
|
+
image: images[0].image,
|
|
425
|
+
elapsed,
|
|
426
|
+
filename: images[0].filename,
|
|
427
|
+
requestId,
|
|
428
|
+
providerUrl: images[0].providerUrl ?? null,
|
|
429
|
+
createdAt: images[0].createdAt,
|
|
430
|
+
...extra,
|
|
431
|
+
});
|
|
435
432
|
}
|
|
436
433
|
else {
|
|
437
434
|
finishHttpStatus = 200;
|
|
@@ -441,7 +438,7 @@ export function registerGenerateRoutes(app, ctxRaw) {
|
|
|
441
438
|
imageCount: images.length,
|
|
442
439
|
elapsedMs: Date.now() - startTime,
|
|
443
440
|
});
|
|
444
|
-
|
|
441
|
+
succeed({ images, elapsed, count: images.length, requestId, ...extra });
|
|
445
442
|
}
|
|
446
443
|
}
|
|
447
444
|
catch (e) {
|
|
@@ -451,41 +448,18 @@ export function registerGenerateRoutes(app, ctxRaw) {
|
|
|
451
448
|
if (isGenerationCanceledError(err.raw) || isJobCanceled(requestId)) {
|
|
452
449
|
const canceled = makeGenerationCanceledError();
|
|
453
450
|
finishCanceled = true;
|
|
454
|
-
|
|
455
|
-
finishErrorCode = canceled.code;
|
|
456
|
-
return res.status(canceled.status).json({
|
|
451
|
+
return fail(canceled.status, {
|
|
457
452
|
error: canceled.message,
|
|
458
453
|
code: canceled.code,
|
|
459
454
|
requestId,
|
|
460
455
|
});
|
|
461
456
|
}
|
|
462
|
-
finishStatus = "error";
|
|
463
|
-
finishHttpStatus = err.status || 500;
|
|
464
457
|
finishErrorCode = fallbackCode || "GENERATE_FAILED";
|
|
465
458
|
logError("generate", "error", err.raw, { requestId, code: finishErrorCode });
|
|
466
|
-
|
|
459
|
+
fail(err.status || 500, {
|
|
467
460
|
error: err.message,
|
|
468
|
-
code:
|
|
469
|
-
|
|
470
|
-
upstreamType: ext.upstreamType || null,
|
|
471
|
-
upstreamParam: ext.upstreamParam || null,
|
|
472
|
-
diagnosticReason: ext.diagnosticReason || null,
|
|
473
|
-
retryKind: ext.retryKind || null,
|
|
474
|
-
initialEventCount: ext.initialEventCount ?? null,
|
|
475
|
-
initialEventTypes: ext.initialEventTypes || null,
|
|
476
|
-
referencesDroppedOnRetry: ext.referencesDroppedOnRetry ?? null,
|
|
477
|
-
developerPromptDroppedOnRetry: ext.developerPromptDroppedOnRetry ?? null,
|
|
478
|
-
webSearchDroppedOnRetry: ext.webSearchDroppedOnRetry ?? null,
|
|
479
|
-
fallbackEventCount: ext.fallbackEventCount ?? null,
|
|
480
|
-
fallbackEventTypes: ext.fallbackEventTypes || null,
|
|
481
|
-
fallbackImageCallSeen: ext.fallbackImageCallSeen ?? null,
|
|
482
|
-
fallbackImageResultCount: ext.fallbackImageResultCount ?? null,
|
|
483
|
-
errorEventCount: ext.eventCount ?? null,
|
|
484
|
-
eventTypes: ext.eventTypes || null,
|
|
485
|
-
webSearchCalls: ext.webSearchCalls ?? null,
|
|
486
|
-
responseDiagnostics: ext.responseDiagnostics || null,
|
|
487
|
-
toolTypes: ext.toolTypes || null,
|
|
488
|
-
toolChoiceKind: ext.toolChoiceKind || null,
|
|
461
|
+
code: finishErrorCode,
|
|
462
|
+
...upstreamErrorFields(ext),
|
|
489
463
|
requestId,
|
|
490
464
|
});
|
|
491
465
|
}
|
|
@@ -497,6 +471,15 @@ export function registerGenerateRoutes(app, ctxRaw) {
|
|
|
497
471
|
errorCode: finishErrorCode,
|
|
498
472
|
meta: finishMeta,
|
|
499
473
|
});
|
|
474
|
+
appendGenerationRequestLog(ctx.config.storage.generationRequestLogFile, {
|
|
475
|
+
id: randomBytes(8).toString("hex"),
|
|
476
|
+
requestId,
|
|
477
|
+
createdAt: Date.now(),
|
|
478
|
+
prompt: typeof req.body?.prompt === "string" ? req.body.prompt : "",
|
|
479
|
+
requested: parseInt(req.body?.n) || 1,
|
|
480
|
+
succeeded: finishStatus === "completed" ? (finishMeta.imageCount ?? 1) : 0,
|
|
481
|
+
error: finishStatus === "error" ? (finishErrorCode ?? "unknown") : null,
|
|
482
|
+
}).catch(() => { });
|
|
500
483
|
}
|
|
501
484
|
});
|
|
502
485
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { listGenerationRequestLog } from "../lib/generationRequestLog.js";
|
|
2
|
+
import { requireRuntimeContext } from "../lib/runtimeContext.js";
|
|
3
|
+
export function registerGenerationRequestLogRoutes(app, ctxRaw) {
|
|
4
|
+
const ctx = requireRuntimeContext(ctxRaw);
|
|
5
|
+
app.get("/api/generation-requests", async (_req, res) => {
|
|
6
|
+
try {
|
|
7
|
+
const items = await listGenerationRequestLog(ctx.config.storage.generationRequestLogFile);
|
|
8
|
+
res.json({ items });
|
|
9
|
+
}
|
|
10
|
+
catch (error) {
|
|
11
|
+
res.status(500).json({
|
|
12
|
+
error: error instanceof Error ? error.message : "Could not read generation request log",
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
}
|
package/routes/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { registerCapabilitiesRoutes } from "./capabilities.js";
|
|
2
|
+
import { registerEventsRoute } from "./events.js";
|
|
2
3
|
import { registerHealthRoutes } from "./health.js";
|
|
3
4
|
import { registerHistoryRoutes } from "./history.js";
|
|
4
5
|
import { registerSessionRoutes } from "./sessions.js";
|
|
@@ -18,14 +19,17 @@ import { registerImageImportRoutes } from "./imageImport.js";
|
|
|
18
19
|
import { registerPromptBuilderRoutes } from "./promptBuilder.js";
|
|
19
20
|
import { registerAgentRoutes } from "./agent.js";
|
|
20
21
|
import { registerGrokRoutes } from "./grok.js";
|
|
22
|
+
import { registerAgyRoutes } from "./agy.js";
|
|
21
23
|
import { registerVideoRoutes } from "./video.js";
|
|
22
24
|
import { registerVideoExtendedRoutes } from "./videoExtended.js";
|
|
23
25
|
import { registerQuotaRoutes } from "./quota.js";
|
|
24
26
|
import { registerAuthRoutes } from "./auth.js";
|
|
25
27
|
import { mountKeyRoutes } from "./keys.js";
|
|
28
|
+
import { registerGenerationRequestLogRoutes } from "./generationRequestLog.js";
|
|
26
29
|
import { requireRuntimeContext } from "../lib/runtimeContext.js";
|
|
27
30
|
export function configureRoutes(app, ctxRaw) {
|
|
28
31
|
const ctx = requireRuntimeContext(ctxRaw);
|
|
32
|
+
registerEventsRoute(app, ctx);
|
|
29
33
|
registerHealthRoutes(app, ctx);
|
|
30
34
|
registerCapabilitiesRoutes(app, ctx);
|
|
31
35
|
registerStorageRoutes(app, ctx);
|
|
@@ -47,9 +51,11 @@ export function configureRoutes(app, ctxRaw) {
|
|
|
47
51
|
registerPromptRoutes(app, ctx);
|
|
48
52
|
registerPromptImportRoutes(app, ctx);
|
|
49
53
|
registerGrokRoutes(app, ctx);
|
|
54
|
+
registerAgyRoutes(app);
|
|
50
55
|
registerVideoRoutes(app, ctx);
|
|
51
56
|
registerVideoExtendedRoutes(app, ctx);
|
|
52
57
|
registerQuotaRoutes(app, ctx);
|
|
53
58
|
registerAuthRoutes(app);
|
|
54
59
|
mountKeyRoutes(app, ctx);
|
|
60
|
+
registerGenerationRequestLogRoutes(app, ctx);
|
|
55
61
|
}
|
package/routes/keys.js
CHANGED
|
@@ -62,8 +62,28 @@ export function mountKeyRoutes(app, ctx) {
|
|
|
62
62
|
valid: !!vertexJson,
|
|
63
63
|
maskedKey: ctx.vertexProjectId ? `project: ${ctx.vertexProjectId}` : null,
|
|
64
64
|
};
|
|
65
|
+
status.geminiAuthMode = ctx.geminiAuthMode
|
|
66
|
+
|| (vertexJson && !ctx.geminiApiKey ? "vertex" : "apikey");
|
|
65
67
|
res.json(status);
|
|
66
68
|
});
|
|
69
|
+
// Persist the Gemini auth mode chosen in the settings dropdown, so reopening
|
|
70
|
+
// settings (or restarting the server) keeps the user's selection.
|
|
71
|
+
app.put("/api/keys/gemini-auth-mode", async (req, res) => {
|
|
72
|
+
const { mode } = req.body;
|
|
73
|
+
if (mode !== "apikey" && mode !== "vertex") {
|
|
74
|
+
return res.status(400).json({ ok: false, error: "mode must be apikey|vertex", code: "INVALID_MODE" });
|
|
75
|
+
}
|
|
76
|
+
const cfgPath = ctx.config.storage.configFile;
|
|
77
|
+
let existing = {};
|
|
78
|
+
try {
|
|
79
|
+
existing = JSON.parse(await readFile(cfgPath, "utf-8"));
|
|
80
|
+
}
|
|
81
|
+
catch { /* new file */ }
|
|
82
|
+
existing.geminiAuthMode = mode;
|
|
83
|
+
await writeConfigAtomic(cfgPath, existing);
|
|
84
|
+
ctx.geminiAuthMode = mode;
|
|
85
|
+
return res.json({ ok: true, geminiAuthMode: mode });
|
|
86
|
+
});
|
|
67
87
|
// Vertex JSON — dedicated route (before generic :provider)
|
|
68
88
|
app.put("/api/keys/vertex", async (req, res) => {
|
|
69
89
|
const { serviceAccountJson } = req.body;
|
|
@@ -187,6 +207,8 @@ export function mountKeyRoutes(app, ctx) {
|
|
|
187
207
|
}
|
|
188
208
|
catch { /* new file */ }
|
|
189
209
|
existing[CONFIG_KEY_MAP[provider]] = trimmed;
|
|
210
|
+
if (provider === "gemini")
|
|
211
|
+
existing.geminiAuthMode = "apikey";
|
|
190
212
|
await writeConfigAtomic(cfgPath, existing);
|
|
191
213
|
// Hot-update runtime context
|
|
192
214
|
if (provider === "openai") {
|
|
@@ -209,8 +231,6 @@ export function mountKeyRoutes(app, ctx) {
|
|
|
209
231
|
ctx.geminiApiKeySource = "config";
|
|
210
232
|
ctx.hasGeminiApiKey = true;
|
|
211
233
|
ctx.geminiAuthMode = "apikey";
|
|
212
|
-
existing[CONFIG_KEY_MAP[provider]] = trimmed;
|
|
213
|
-
existing.geminiAuthMode = "apikey";
|
|
214
234
|
}
|
|
215
235
|
return res.json({ ok: true, provider, source: "config", valid: true });
|
|
216
236
|
});
|