ima2-gen 2.0.0 → 2.0.2
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 +150 -0
- package/README.md +12 -12
- package/bin/commands/backfillThumbs.js +24 -0
- package/bin/commands/edit.js +7 -6
- package/bin/commands/gen.js +13 -6
- package/bin/commands/multimode.js +5 -4
- package/bin/commands/node.js +4 -4
- package/bin/ima2.js +21 -11
- package/bin/lib/config-store.js +1 -1
- package/docs/API.md +184 -10
- package/docs/CLI.md +11 -4
- package/docs/FAQ.ko.md +16 -0
- package/docs/FAQ.md +30 -0
- package/docs/PROMPT_STUDIO.md +3 -1
- package/docs/README.ko.md +7 -3
- package/docs/migration/runtime-test-inventory.md +17 -1
- package/lib/agentImageVideoGen.js +261 -0
- package/lib/agentRuntime.js +11 -260
- package/lib/agentSettings.js +1 -1
- package/lib/agyImageAdapter.js +259 -0
- package/lib/capabilities.js +2 -1
- package/lib/configKeys.js +1 -1
- package/lib/errorClassify.js +8 -7
- package/lib/eventBus.js +71 -0
- package/lib/geminiApiImageAdapter.js +179 -0
- package/lib/generationErrors.js +3 -1
- package/lib/grokImageAdapter.js +74 -128
- package/lib/grokImageCore.js +153 -0
- package/lib/grokMultimodeAdapter.js +7 -4
- package/lib/grokRuntime.js +3 -0
- package/lib/grokSizeMapper.js +13 -1
- package/lib/grokVideoAdapter.js +14 -7
- package/lib/grokVideoCanvas.js +13 -0
- package/lib/grokVideoPlannerPrompt.js +53 -6
- package/lib/historyList.js +19 -2
- package/lib/imageModels.js +15 -0
- package/lib/imageThumb.js +38 -0
- package/lib/inflight.js +54 -17
- package/lib/multimodeHelpers.js +10 -0
- package/lib/nodeHelpers.js +59 -0
- package/lib/oauthProxy/prompts.js +30 -36
- package/lib/promptBuilder/systemPrompt.js +2 -5
- package/lib/promptSafetyPolicy.js +1 -5
- package/lib/providerOptions.js +36 -1
- package/lib/responsesFallback.js +53 -44
- package/lib/routeHelpers.js +44 -0
- package/lib/runtimeContext.js +27 -0
- package/lib/ssePublish.js +12 -0
- package/lib/storageMigration.js +1 -1
- package/lib/storyboardPrefix.js +28 -0
- package/lib/thumbBackfill.js +70 -0
- package/lib/vertexAuth.js +44 -0
- package/lib/videoThumb.js +60 -0
- package/package.json +7 -2
- package/routes/agy.js +44 -0
- package/routes/auth.js +242 -0
- package/routes/edit.js +48 -8
- package/routes/events.js +78 -0
- package/routes/generate.js +135 -135
- package/routes/history.js +13 -0
- package/routes/index.js +8 -0
- package/routes/keys.js +254 -0
- package/routes/multimode.js +138 -62
- package/routes/nodes.js +107 -129
- package/routes/quota.js +58 -7
- package/routes/video.js +107 -20
- package/server.js +123 -0
- package/skills/ima2/SKILL.md +98 -21
- package/ui/dist/.vite/manifest.json +12 -12
- package/ui/dist/assets/AgentWorkspace-Dth6YijN.js +3 -0
- package/ui/dist/assets/{CardNewsWorkspace-BN-ga1lG.js → CardNewsWorkspace-Dav3K5CT.js} +2 -2
- package/ui/dist/assets/{NodeCanvas-BbMa4IhI.js → NodeCanvas-C4ifFzB1.js} +2 -2
- package/ui/dist/assets/{PromptBuilderPanel-DRwBJRDQ.js → PromptBuilderPanel-CEcyU9PL.js} +1 -1
- package/ui/dist/assets/{PromptImportDialog-Dp85kHCq.js → PromptImportDialog-CgQ94Gth.js} +2 -2
- package/ui/dist/assets/{PromptImportDiscoverySection-BE8Q8MLD.js → PromptImportDiscoverySection-CuzyzbNI.js} +1 -1
- package/ui/dist/assets/{PromptImportFolderSection-PtH5x0sc.js → PromptImportFolderSection-DHLGlO6l.js} +1 -1
- package/ui/dist/assets/{PromptLibraryPanel-FnM9tHI9.js → PromptLibraryPanel-BOe18we8.js} +2 -2
- package/ui/dist/assets/SettingsWorkspace-Cdgnm4Wa.js +1 -0
- package/ui/dist/assets/index-C5PSahkr.js +1 -0
- package/ui/dist/assets/index-Dn2AhL6d.css +1 -0
- package/ui/dist/assets/index-Tjqx6wUV.js +23 -0
- package/ui/dist/index.html +2 -2
- package/ui/dist/assets/AgentWorkspace-C21zqdTZ.js +0 -3
- package/ui/dist/assets/SettingsWorkspace-MARPGyBL.js +0 -1
- package/ui/dist/assets/index-BAFI6htx.js +0 -42
- package/ui/dist/assets/index-BSXxr_Bt.js +0 -1
- package/ui/dist/assets/index-DS-ADE7U.css +0 -1
package/routes/generate.js
CHANGED
|
@@ -3,13 +3,16 @@ import { safeWriteSidecar, atomicWriteJson } from "../lib/atomicWrite.js";
|
|
|
3
3
|
import { join } from "path";
|
|
4
4
|
import { randomBytes } from "crypto";
|
|
5
5
|
import { detectImageMimeFromB64, summarizeReferencePayload, validateAndNormalizeRefs } from "../lib/refs.js";
|
|
6
|
+
import { generateImageThumbnailFromBuffer } from "../lib/imageThumb.js";
|
|
6
7
|
import { classifyUpstreamError } from "../lib/errorClassify.js";
|
|
7
8
|
import { normalizeOAuthParams } from "../lib/oauthNormalize.js";
|
|
8
9
|
import { resolveProviderOptions } from "../lib/providerOptions.js";
|
|
9
10
|
import { generateViaResponses } from "../lib/responsesImageAdapter.js";
|
|
10
11
|
import { generateViaGrok, planGrokImage } from "../lib/grokImageAdapter.js";
|
|
12
|
+
import { generateViaAgy } from "../lib/agyImageAdapter.js";
|
|
13
|
+
import { generateViaGeminiApi } from "../lib/geminiApiImageAdapter.js";
|
|
11
14
|
import { isNonRetryableGenerationError, normalizeGenerationFailure } from "../lib/generationErrors.js";
|
|
12
|
-
import { startJob, finishJob, registerJobAbortController, isJobCanceled } from "../lib/inflight.js";
|
|
15
|
+
import { startJob, finishJob, registerJobAbortController, isJobCanceled, isStartJobFailure, setJobPhase, INFLIGHT_RETRY_AFTER_SECONDS, } from "../lib/inflight.js";
|
|
13
16
|
import { isGenerationCanceledError, makeGenerationCanceledError, throwIfJobCanceled, } from "../lib/generationCancel.js";
|
|
14
17
|
import { logEvent, logError } from "../lib/logger.js";
|
|
15
18
|
import { embedImageMetadataBestEffort } from "../lib/imageMetadataStore.js";
|
|
@@ -17,57 +20,44 @@ import { invalidateHistoryIndex } from "../lib/historyIndex.js";
|
|
|
17
20
|
import { normalizeComposerInsertedPrompts, normalizeComposerPrompt, } from "../lib/composerSnapshot.js";
|
|
18
21
|
import { errInfo } from "../lib/errInfo.js";
|
|
19
22
|
import { requireRuntimeContext } from "../lib/runtimeContext.js";
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
return { moderation };
|
|
25
|
-
}
|
|
26
|
-
function imageFormatFromMime(mime) {
|
|
27
|
-
if (mime === "image/jpeg")
|
|
28
|
-
return "jpeg";
|
|
29
|
-
if (mime === "image/webp")
|
|
30
|
-
return "webp";
|
|
31
|
-
return "png";
|
|
32
|
-
}
|
|
23
|
+
import { STORYBOARD_PREFIX } from "../lib/storyboardPrefix.js";
|
|
24
|
+
import { validateModeration, imageFormatFromMime, upstreamErrorFields } from "../lib/routeHelpers.js";
|
|
25
|
+
import { publish } from "../lib/eventBus.js";
|
|
26
|
+
import { publishJobEvent } from "../lib/ssePublish.js";
|
|
33
27
|
export function registerGenerateRoutes(app, ctxRaw) {
|
|
34
28
|
const ctx = requireRuntimeContext(ctxRaw);
|
|
35
29
|
app.post("/api/generate", async (req, res) => {
|
|
36
30
|
const requestId = typeof req.body?.requestId === "string" ? req.body.requestId : req.id;
|
|
31
|
+
const asyncMode = req.body?.async === true;
|
|
37
32
|
let finishStatus = "completed";
|
|
38
33
|
let finishHttpStatus;
|
|
39
34
|
let finishErrorCode;
|
|
40
35
|
let finishMeta = {};
|
|
41
36
|
let finishCanceled = false;
|
|
42
37
|
const cancelController = new AbortController();
|
|
38
|
+
const fail = (status, payload) => {
|
|
39
|
+
finishStatus = "error";
|
|
40
|
+
finishHttpStatus = status;
|
|
41
|
+
finishErrorCode = typeof payload.code === "string" ? payload.code : finishErrorCode;
|
|
42
|
+
if (asyncMode && res.headersSent) {
|
|
43
|
+
publish(requestId, "error", { ...payload, status, requestId });
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
return res.status(status).json(payload);
|
|
47
|
+
};
|
|
48
|
+
const succeed = (payload) => {
|
|
49
|
+
if (asyncMode) {
|
|
50
|
+
publishJobEvent(requestId, "done", payload);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
res.json(payload);
|
|
54
|
+
};
|
|
43
55
|
try {
|
|
44
56
|
const sessionId = typeof req.body?.sessionId === "string" ? req.body.sessionId : null;
|
|
45
57
|
const clientNodeId = typeof req.body?.clientNodeId === "string" ? req.body.clientNodeId : null;
|
|
46
58
|
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;
|
|
47
59
|
const storyboardActive = req.body?.storyboard === true;
|
|
48
|
-
const storyboardPrefix = storyboardActive
|
|
49
|
-
? [
|
|
50
|
-
"[STORYBOARD MODE — Video Production Keyframe]",
|
|
51
|
-
"This image is a keyframe for a multi-shot VIDEO storyboard. It will be animated via image-to-video.",
|
|
52
|
-
"The prompt and all injected instructions MUST be in English.",
|
|
53
|
-
"",
|
|
54
|
-
"CHARACTER LOCK:",
|
|
55
|
-
"- Identify each character by 2-3 VISUAL identifiers (clothing color + physique + position/props). Never by name alone.",
|
|
56
|
-
"- Copy character descriptions VERBATIM from the reference/prior frame. Do NOT rephrase or drift.",
|
|
57
|
-
"",
|
|
58
|
-
"SCENE CONTINUITY:",
|
|
59
|
-
"- Lock lighting direction, color palette, environment, and art style to prior frames.",
|
|
60
|
-
"- Change ONLY: action, shot scale, camera angle, or expression.",
|
|
61
|
-
"- Reference image = canonical anchor. Preserve it faithfully.",
|
|
62
|
-
"",
|
|
63
|
-
"VIDEO-READY COMPOSITION:",
|
|
64
|
-
"- Frame for animation: leave space for motion, avoid static-only poses.",
|
|
65
|
-
"- Use descriptive caption format: shot type + subject action + environment + technical (lens, lighting) + mood.",
|
|
66
|
-
"- Specify intended camera movement for the video phase (e.g. 'slow dolly-in', 'static wide').",
|
|
67
|
-
"- End pose must be stable and suitable for video continuation.",
|
|
68
|
-
"",
|
|
69
|
-
].join("\n") + "\n"
|
|
70
|
-
: "";
|
|
60
|
+
const storyboardPrefix = storyboardActive ? STORYBOARD_PREFIX : "";
|
|
71
61
|
const composerPrompt = normalizeComposerPrompt(req.body?.composerPrompt);
|
|
72
62
|
const composerInsertedPrompts = normalizeComposerInsertedPrompts(req.body?.composerInsertedPrompts);
|
|
73
63
|
const { quality, warnings: qualityWarnings } = normalizeOAuthParams({ provider, quality: rawQuality });
|
|
@@ -79,10 +69,7 @@ export function registerGenerateRoutes(app, ctxRaw) {
|
|
|
79
69
|
rawWebSearchEnabled,
|
|
80
70
|
});
|
|
81
71
|
if (providerOptions.error) {
|
|
82
|
-
|
|
83
|
-
finishHttpStatus = providerOptions.status;
|
|
84
|
-
finishErrorCode = providerOptions.code;
|
|
85
|
-
return res.status(providerOptions.status).json({ error: providerOptions.error, code: providerOptions.code });
|
|
72
|
+
return fail(providerOptions.status, { error: providerOptions.error, code: providerOptions.code });
|
|
86
73
|
}
|
|
87
74
|
const imageModel = providerOptions.model;
|
|
88
75
|
const reasoningEffort = providerOptions.reasoningEffort;
|
|
@@ -92,13 +79,34 @@ export function registerGenerateRoutes(app, ctxRaw) {
|
|
|
92
79
|
const normalizedPromptMode = promptMode === "direct" ? "direct" : "auto";
|
|
93
80
|
const generationPrompt = storyboardPrefix + prompt;
|
|
94
81
|
if (!prompt)
|
|
95
|
-
return
|
|
82
|
+
return fail(400, { error: "Prompt is required" });
|
|
96
83
|
const moderationCheck = validateModeration(ctx, moderation);
|
|
97
84
|
if (moderationCheck.error)
|
|
98
|
-
return
|
|
85
|
+
return fail(400, { error: moderationCheck.error });
|
|
99
86
|
const count = Math.min(Math.max(parseInt(n) || 1, 1), 8);
|
|
100
87
|
const referencePayload = summarizeReferencePayload(references);
|
|
101
|
-
|
|
88
|
+
const refCheckResult = validateAndNormalizeRefs(references);
|
|
89
|
+
if (refCheckResult.error) {
|
|
90
|
+
return fail(400, { error: refCheckResult.error, code: refCheckResult.code });
|
|
91
|
+
}
|
|
92
|
+
const refCheck = refCheckResult;
|
|
93
|
+
const incomingProviderUrl = typeof req.body?.providerUrl === "string" && req.body.providerUrl.startsWith("http")
|
|
94
|
+
? req.body.providerUrl
|
|
95
|
+
: null;
|
|
96
|
+
const grokRefs = incomingProviderUrl
|
|
97
|
+
? [{ b64: "", url: incomingProviderUrl, declaredMime: "image/png", detectedMime: "image/png" }, ...refCheck.refDetails]
|
|
98
|
+
: refCheck.refDetails;
|
|
99
|
+
const providerRefCount = activeProvider === "grok" || activeProvider === "grok-api"
|
|
100
|
+
? grokRefs.length
|
|
101
|
+
: refCheck.refs.length;
|
|
102
|
+
if ((activeProvider === "grok" || activeProvider === "agy" || activeProvider === "grok-api" || activeProvider === "gemini-api") && providerRefCount > 3) {
|
|
103
|
+
return fail(400, {
|
|
104
|
+
error: `${activeProvider === "agy" ? "Agy" : "Grok"} image editing supports up to 3 reference images`,
|
|
105
|
+
code: activeProvider === "agy" ? "AGY_REF_TOO_MANY" : "GROK_REF_TOO_MANY",
|
|
106
|
+
requestId,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
const started = startJob({
|
|
102
110
|
requestId,
|
|
103
111
|
kind: "classic",
|
|
104
112
|
prompt,
|
|
@@ -111,32 +119,33 @@ export function registerGenerateRoutes(app, ctxRaw) {
|
|
|
111
119
|
model: imageModel,
|
|
112
120
|
size: effectiveSize,
|
|
113
121
|
n: count,
|
|
114
|
-
refsCount:
|
|
122
|
+
refsCount: providerRefCount,
|
|
115
123
|
referenceBytes: referencePayload.referenceBytes,
|
|
116
124
|
referenceB64Chars: referencePayload.referenceB64Chars,
|
|
117
125
|
composerPrompt,
|
|
118
126
|
composerInsertedPrompts,
|
|
119
127
|
},
|
|
120
128
|
});
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
finishStatus = "error";
|
|
132
|
-
finishHttpStatus = 400;
|
|
133
|
-
finishErrorCode = "GROK_REF_TOO_MANY";
|
|
134
|
-
return res.status(400).json({
|
|
135
|
-
error: "Grok image editing supports up to 3 reference images",
|
|
136
|
-
code: "GROK_REF_TOO_MANY",
|
|
129
|
+
if (started && isStartJobFailure(started)) {
|
|
130
|
+
const status = started.code === "TOO_MANY_JOBS" ? 429 : 409;
|
|
131
|
+
if (started.code === "TOO_MANY_JOBS") {
|
|
132
|
+
res.setHeader("Retry-After", String(INFLIGHT_RETRY_AFTER_SECONDS));
|
|
133
|
+
}
|
|
134
|
+
return fail(status, {
|
|
135
|
+
error: started.code === "TOO_MANY_JOBS"
|
|
136
|
+
? "Too many concurrent generation jobs"
|
|
137
|
+
: "Request ID already in use",
|
|
138
|
+
code: started.code,
|
|
137
139
|
requestId,
|
|
138
140
|
});
|
|
139
141
|
}
|
|
142
|
+
registerJobAbortController(requestId, cancelController);
|
|
143
|
+
if (asyncMode) {
|
|
144
|
+
res.status(202).json({ requestId, async: true });
|
|
145
|
+
}
|
|
146
|
+
setJobPhase(requestId, "streaming");
|
|
147
|
+
if (asyncMode)
|
|
148
|
+
publish(requestId, "phase", { requestId, phase: "streaming" });
|
|
140
149
|
const client = req.get("x-ima2-client") || "ui";
|
|
141
150
|
const referenceDiagnostics = refCheck.referenceDiagnostics || [];
|
|
142
151
|
const referenceMismatchCount = referenceDiagnostics.filter((ref) => ref.warnings?.includes("mime_mismatch")).length;
|
|
@@ -149,7 +158,7 @@ export function registerGenerateRoutes(app, ctxRaw) {
|
|
|
149
158
|
size: effectiveSize,
|
|
150
159
|
moderation,
|
|
151
160
|
n: count,
|
|
152
|
-
refs:
|
|
161
|
+
refs: providerRefCount,
|
|
153
162
|
referenceBytes: referencePayload.referenceBytes,
|
|
154
163
|
referenceMismatchCount,
|
|
155
164
|
refDetectedMimes: [...new Set(referenceDiagnostics.map((ref) => ref.detectedMime).filter(Boolean))].join(","),
|
|
@@ -162,21 +171,43 @@ export function registerGenerateRoutes(app, ctxRaw) {
|
|
|
162
171
|
});
|
|
163
172
|
const startTime = Date.now();
|
|
164
173
|
const mimeMap = { png: "image/png", jpeg: "image/jpeg", webp: "image/webp" };
|
|
165
|
-
const effectiveFormat = activeProvider === "grok" ? "jpeg" : String(format);
|
|
174
|
+
const effectiveFormat = activeProvider === "grok" || activeProvider === "agy" || activeProvider === "grok-api" || activeProvider === "gemini-api" ? "jpeg" : String(format);
|
|
166
175
|
const mime = mimeMap[effectiveFormat] || "image/png";
|
|
167
176
|
await mkdir(ctx.config.storage.generatedDir, { recursive: true });
|
|
168
|
-
const
|
|
177
|
+
const grokDirectApiKey = activeProvider === "grok-api" ? ctx.xaiApiKey : undefined;
|
|
178
|
+
const sharedGrokPlan = activeProvider === "grok" || activeProvider === "grok-api"
|
|
169
179
|
? await planGrokImage(generationPrompt, ctx, {
|
|
170
180
|
model: quality === "high" ? "grok-imagine-image-quality" : imageModel,
|
|
171
181
|
size: effectiveSize,
|
|
172
182
|
signal: cancelController.signal,
|
|
173
183
|
requestId,
|
|
174
|
-
referenceCount:
|
|
175
|
-
references:
|
|
184
|
+
referenceCount: grokRefs.length,
|
|
185
|
+
references: grokRefs,
|
|
186
|
+
directApiKey: grokDirectApiKey,
|
|
176
187
|
})
|
|
177
188
|
: null;
|
|
178
189
|
const generateOne = async () => {
|
|
179
|
-
if (activeProvider === "
|
|
190
|
+
if (activeProvider === "gemini-api") {
|
|
191
|
+
const r = await generateViaGeminiApi(generationPrompt, requireRuntimeContext(ctx), {
|
|
192
|
+
model: imageModel,
|
|
193
|
+
size: effectiveSize,
|
|
194
|
+
signal: cancelController.signal,
|
|
195
|
+
requestId,
|
|
196
|
+
references: refCheck.refDetails,
|
|
197
|
+
});
|
|
198
|
+
throwIfJobCanceled(requestId);
|
|
199
|
+
return r;
|
|
200
|
+
}
|
|
201
|
+
if (activeProvider === "agy") {
|
|
202
|
+
const r = await generateViaAgy(generationPrompt, {
|
|
203
|
+
references: refCheck.refDetails,
|
|
204
|
+
signal: cancelController.signal,
|
|
205
|
+
requestId,
|
|
206
|
+
});
|
|
207
|
+
throwIfJobCanceled(requestId);
|
|
208
|
+
return r;
|
|
209
|
+
}
|
|
210
|
+
if (activeProvider === "grok" || activeProvider === "grok-api") {
|
|
180
211
|
const grokModel = quality === "high" ? "grok-imagine-image-quality" : imageModel;
|
|
181
212
|
const r = await generateViaGrok(generationPrompt, ctx, {
|
|
182
213
|
model: grokModel,
|
|
@@ -185,7 +216,8 @@ export function registerGenerateRoutes(app, ctxRaw) {
|
|
|
185
216
|
requestId,
|
|
186
217
|
plannedPrompt: sharedGrokPlan?.prompt,
|
|
187
218
|
webSearchCalls: sharedGrokPlan?.webSearchCalls,
|
|
188
|
-
references:
|
|
219
|
+
references: grokRefs,
|
|
220
|
+
directApiKey: grokDirectApiKey,
|
|
189
221
|
});
|
|
190
222
|
throwIfJobCanceled(requestId);
|
|
191
223
|
return r;
|
|
@@ -232,10 +264,10 @@ export function registerGenerateRoutes(app, ctxRaw) {
|
|
|
232
264
|
if (r.status === "fulfilled" && r.value.b64) {
|
|
233
265
|
throwIfJobCanceled(requestId);
|
|
234
266
|
const valueWithMime = r.value;
|
|
235
|
-
const resultMime = activeProvider === "grok"
|
|
267
|
+
const resultMime = activeProvider === "grok" || activeProvider === "agy" || activeProvider === "grok-api" || activeProvider === "gemini-api"
|
|
236
268
|
? (valueWithMime.mime || detectImageMimeFromB64(r.value.b64) || mime)
|
|
237
269
|
: mime;
|
|
238
|
-
const resultFormat = activeProvider === "grok" ? imageFormatFromMime(resultMime) : effectiveFormat;
|
|
270
|
+
const resultFormat = activeProvider === "grok" || activeProvider === "agy" || activeProvider === "grok-api" || activeProvider === "gemini-api" ? imageFormatFromMime(resultMime) : effectiveFormat;
|
|
239
271
|
const retryValue = r.value;
|
|
240
272
|
if (!firstRetryMeta && retryValue.retryKind) {
|
|
241
273
|
firstRetryMeta = {
|
|
@@ -249,6 +281,11 @@ export function registerGenerateRoutes(app, ctxRaw) {
|
|
|
249
281
|
}
|
|
250
282
|
const rand = randomBytes(ctx.config.ids.generatedHexBytes).toString("hex");
|
|
251
283
|
const filename = `${Date.now()}_${rand}_${images.length}.${resultFormat}`;
|
|
284
|
+
const createdAt = Date.now();
|
|
285
|
+
const valueWithProviderUrl = r.value;
|
|
286
|
+
const providerUrl = typeof valueWithProviderUrl.providerUrl === "string"
|
|
287
|
+
? valueWithProviderUrl.providerUrl
|
|
288
|
+
: undefined;
|
|
252
289
|
const meta = {
|
|
253
290
|
kind: "classic",
|
|
254
291
|
requestId,
|
|
@@ -267,11 +304,12 @@ export function registerGenerateRoutes(app, ctxRaw) {
|
|
|
267
304
|
model: activeProvider === "grok" ? (quality === "high" ? "grok-imagine-image-quality" : imageModel) : imageModel,
|
|
268
305
|
reasoningEffort,
|
|
269
306
|
provider: activeProvider,
|
|
270
|
-
createdAt
|
|
307
|
+
createdAt,
|
|
308
|
+
...(providerUrl ? { providerUrl } : {}),
|
|
271
309
|
usage: r.value.usage || null,
|
|
272
310
|
webSearchCalls: r.value.webSearchCalls || 0,
|
|
273
311
|
webSearchEnabled,
|
|
274
|
-
refsCount:
|
|
312
|
+
refsCount: providerRefCount,
|
|
275
313
|
};
|
|
276
314
|
const rawBuffer = Buffer.from(r.value.b64, "base64");
|
|
277
315
|
const embedded = await embedImageMetadataBestEffort(rawBuffer, resultFormat, meta, {
|
|
@@ -285,13 +323,17 @@ export function registerGenerateRoutes(app, ctxRaw) {
|
|
|
285
323
|
warning: embedded.warning,
|
|
286
324
|
});
|
|
287
325
|
}
|
|
288
|
-
|
|
289
|
-
await
|
|
326
|
+
const filePath = join(ctx.config.storage.generatedDir, filename);
|
|
327
|
+
await writeFile(filePath, embedded.buffer);
|
|
328
|
+
await safeWriteSidecar(filePath + ".json", meta);
|
|
329
|
+
generateImageThumbnailFromBuffer(embedded.buffer, filePath).catch(() => { });
|
|
290
330
|
invalidateHistoryIndex();
|
|
291
331
|
images.push({
|
|
292
332
|
image: `data:${resultMime};base64,${r.value.b64}`,
|
|
293
333
|
filename,
|
|
294
334
|
revisedPrompt: r.value.revisedPrompt || null,
|
|
335
|
+
...(providerUrl ? { providerUrl } : {}),
|
|
336
|
+
createdAt,
|
|
295
337
|
});
|
|
296
338
|
if (r.value.usage) {
|
|
297
339
|
const usageValue = r.value.usage;
|
|
@@ -306,7 +348,7 @@ export function registerGenerateRoutes(app, ctxRaw) {
|
|
|
306
348
|
}
|
|
307
349
|
}
|
|
308
350
|
if (typeof r.value.webSearchCalls === "number") {
|
|
309
|
-
totalWebSearchCalls = activeProvider === "grok"
|
|
351
|
+
totalWebSearchCalls = activeProvider === "grok" || activeProvider === "grok-api"
|
|
310
352
|
? Math.max(totalWebSearchCalls, r.value.webSearchCalls)
|
|
311
353
|
: totalWebSearchCalls + r.value.webSearchCalls;
|
|
312
354
|
}
|
|
@@ -321,47 +363,20 @@ export function registerGenerateRoutes(app, ctxRaw) {
|
|
|
321
363
|
const status = firstErr.status || 500;
|
|
322
364
|
if (isGenerationCanceledError(firstErr)) {
|
|
323
365
|
finishCanceled = true;
|
|
324
|
-
|
|
325
|
-
finishErrorCode = firstErr.code;
|
|
326
|
-
return res.status(firstErr.status).json({
|
|
366
|
+
return fail(firstErr.status, {
|
|
327
367
|
error: firstErr.message,
|
|
328
368
|
code: firstErr.code,
|
|
329
369
|
requestId,
|
|
330
370
|
});
|
|
331
371
|
}
|
|
332
|
-
|
|
333
|
-
finishHttpStatus = status;
|
|
334
|
-
finishErrorCode = firstErr.code;
|
|
335
|
-
return res.status(status).json({
|
|
372
|
+
return fail(status, {
|
|
336
373
|
error: firstErr.message,
|
|
337
374
|
code: firstErr.code,
|
|
338
|
-
|
|
339
|
-
upstreamType: firstErr.upstreamType || null,
|
|
340
|
-
upstreamParam: firstErr.upstreamParam || null,
|
|
341
|
-
diagnosticReason: firstErr.diagnosticReason || null,
|
|
342
|
-
retryKind: firstErr.retryKind || null,
|
|
343
|
-
initialEventCount: firstErr.initialEventCount ?? null,
|
|
344
|
-
initialEventTypes: firstErr.initialEventTypes || null,
|
|
345
|
-
referencesDroppedOnRetry: firstErr.referencesDroppedOnRetry ?? null,
|
|
346
|
-
developerPromptDroppedOnRetry: firstErr.developerPromptDroppedOnRetry ?? null,
|
|
347
|
-
webSearchDroppedOnRetry: firstErr.webSearchDroppedOnRetry ?? null,
|
|
348
|
-
fallbackEventCount: firstErr.fallbackEventCount ?? null,
|
|
349
|
-
fallbackEventTypes: firstErr.fallbackEventTypes || null,
|
|
350
|
-
fallbackImageCallSeen: firstErr.fallbackImageCallSeen ?? null,
|
|
351
|
-
fallbackImageResultCount: firstErr.fallbackImageResultCount ?? null,
|
|
352
|
-
errorEventCount: firstErr.eventCount ?? null,
|
|
353
|
-
eventTypes: firstErr.eventTypes || null,
|
|
354
|
-
webSearchCalls: firstErr.webSearchCalls ?? null,
|
|
355
|
-
responseDiagnostics: firstErr.responseDiagnostics || null,
|
|
356
|
-
toolTypes: firstErr.toolTypes || null,
|
|
357
|
-
toolChoiceKind: firstErr.toolChoiceKind || null,
|
|
375
|
+
...upstreamErrorFields(firstErr),
|
|
358
376
|
requestId,
|
|
359
377
|
});
|
|
360
378
|
}
|
|
361
|
-
|
|
362
|
-
finishHttpStatus = 500;
|
|
363
|
-
finishErrorCode = "GENERATE_ALL_FAILED";
|
|
364
|
-
return res.status(500).json({ error: "All generation attempts failed" });
|
|
379
|
+
return fail(500, { error: "All generation attempts failed", code: "GENERATE_ALL_FAILED", requestId });
|
|
365
380
|
}
|
|
366
381
|
const elapsed = +((Date.now() - startTime) / 1000).toFixed(1);
|
|
367
382
|
// Persist elapsed (computed after the generation loop) into each image's sidecar.
|
|
@@ -403,7 +418,15 @@ export function registerGenerateRoutes(app, ctxRaw) {
|
|
|
403
418
|
elapsedMs: Date.now() - startTime,
|
|
404
419
|
filename: images[0].filename,
|
|
405
420
|
});
|
|
406
|
-
|
|
421
|
+
succeed({
|
|
422
|
+
image: images[0].image,
|
|
423
|
+
elapsed,
|
|
424
|
+
filename: images[0].filename,
|
|
425
|
+
requestId,
|
|
426
|
+
providerUrl: images[0].providerUrl ?? null,
|
|
427
|
+
createdAt: images[0].createdAt,
|
|
428
|
+
...extra,
|
|
429
|
+
});
|
|
407
430
|
}
|
|
408
431
|
else {
|
|
409
432
|
finishHttpStatus = 200;
|
|
@@ -413,7 +436,7 @@ export function registerGenerateRoutes(app, ctxRaw) {
|
|
|
413
436
|
imageCount: images.length,
|
|
414
437
|
elapsedMs: Date.now() - startTime,
|
|
415
438
|
});
|
|
416
|
-
|
|
439
|
+
succeed({ images, elapsed, count: images.length, requestId, ...extra });
|
|
417
440
|
}
|
|
418
441
|
}
|
|
419
442
|
catch (e) {
|
|
@@ -423,41 +446,18 @@ export function registerGenerateRoutes(app, ctxRaw) {
|
|
|
423
446
|
if (isGenerationCanceledError(err.raw) || isJobCanceled(requestId)) {
|
|
424
447
|
const canceled = makeGenerationCanceledError();
|
|
425
448
|
finishCanceled = true;
|
|
426
|
-
|
|
427
|
-
finishErrorCode = canceled.code;
|
|
428
|
-
return res.status(canceled.status).json({
|
|
449
|
+
return fail(canceled.status, {
|
|
429
450
|
error: canceled.message,
|
|
430
451
|
code: canceled.code,
|
|
431
452
|
requestId,
|
|
432
453
|
});
|
|
433
454
|
}
|
|
434
|
-
finishStatus = "error";
|
|
435
|
-
finishHttpStatus = err.status || 500;
|
|
436
455
|
finishErrorCode = fallbackCode || "GENERATE_FAILED";
|
|
437
456
|
logError("generate", "error", err.raw, { requestId, code: finishErrorCode });
|
|
438
|
-
|
|
457
|
+
fail(err.status || 500, {
|
|
439
458
|
error: err.message,
|
|
440
|
-
code:
|
|
441
|
-
|
|
442
|
-
upstreamType: ext.upstreamType || null,
|
|
443
|
-
upstreamParam: ext.upstreamParam || null,
|
|
444
|
-
diagnosticReason: ext.diagnosticReason || null,
|
|
445
|
-
retryKind: ext.retryKind || null,
|
|
446
|
-
initialEventCount: ext.initialEventCount ?? null,
|
|
447
|
-
initialEventTypes: ext.initialEventTypes || null,
|
|
448
|
-
referencesDroppedOnRetry: ext.referencesDroppedOnRetry ?? null,
|
|
449
|
-
developerPromptDroppedOnRetry: ext.developerPromptDroppedOnRetry ?? null,
|
|
450
|
-
webSearchDroppedOnRetry: ext.webSearchDroppedOnRetry ?? null,
|
|
451
|
-
fallbackEventCount: ext.fallbackEventCount ?? null,
|
|
452
|
-
fallbackEventTypes: ext.fallbackEventTypes || null,
|
|
453
|
-
fallbackImageCallSeen: ext.fallbackImageCallSeen ?? null,
|
|
454
|
-
fallbackImageResultCount: ext.fallbackImageResultCount ?? null,
|
|
455
|
-
errorEventCount: ext.eventCount ?? null,
|
|
456
|
-
eventTypes: ext.eventTypes || null,
|
|
457
|
-
webSearchCalls: ext.webSearchCalls ?? null,
|
|
458
|
-
responseDiagnostics: ext.responseDiagnostics || null,
|
|
459
|
-
toolTypes: ext.toolTypes || null,
|
|
460
|
-
toolChoiceKind: ext.toolChoiceKind || null,
|
|
459
|
+
code: finishErrorCode,
|
|
460
|
+
...upstreamErrorFields(ext),
|
|
461
461
|
requestId,
|
|
462
462
|
});
|
|
463
463
|
}
|
package/routes/history.js
CHANGED
|
@@ -5,6 +5,7 @@ import { logError, logEvent } from "../lib/logger.js";
|
|
|
5
5
|
import { getDb } from "../lib/db.js";
|
|
6
6
|
import { errInfo } from "../lib/errInfo.js";
|
|
7
7
|
import { requireRuntimeContext } from "../lib/runtimeContext.js";
|
|
8
|
+
import { backfillThumbnails } from "../lib/thumbBackfill.js";
|
|
8
9
|
function asStr(value) {
|
|
9
10
|
return typeof value === "string" ? value : "";
|
|
10
11
|
}
|
|
@@ -160,6 +161,18 @@ export function registerHistoryRoutes(app, ctxRaw) {
|
|
|
160
161
|
res.status(err.status || 500).json({ error: err.message });
|
|
161
162
|
}
|
|
162
163
|
});
|
|
164
|
+
app.post("/api/history/backfill-thumbnails", async (_req, res) => {
|
|
165
|
+
try {
|
|
166
|
+
const r = await backfillThumbnails(ctx.config.storage.generatedDir);
|
|
167
|
+
if (r.created > 0)
|
|
168
|
+
invalidateHistoryIndex();
|
|
169
|
+
res.json({ ok: true, ...r });
|
|
170
|
+
}
|
|
171
|
+
catch (e) {
|
|
172
|
+
const err = errInfo(e);
|
|
173
|
+
res.status(500).json({ error: err.message });
|
|
174
|
+
}
|
|
175
|
+
});
|
|
163
176
|
app.post("/api/history/favorite", async (req, res) => {
|
|
164
177
|
try {
|
|
165
178
|
const db = getDb();
|
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,12 +19,16 @@ 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";
|
|
26
|
+
import { registerAuthRoutes } from "./auth.js";
|
|
27
|
+
import { mountKeyRoutes } from "./keys.js";
|
|
24
28
|
import { requireRuntimeContext } from "../lib/runtimeContext.js";
|
|
25
29
|
export function configureRoutes(app, ctxRaw) {
|
|
26
30
|
const ctx = requireRuntimeContext(ctxRaw);
|
|
31
|
+
registerEventsRoute(app, ctx);
|
|
27
32
|
registerHealthRoutes(app, ctx);
|
|
28
33
|
registerCapabilitiesRoutes(app, ctx);
|
|
29
34
|
registerStorageRoutes(app, ctx);
|
|
@@ -45,7 +50,10 @@ export function configureRoutes(app, ctxRaw) {
|
|
|
45
50
|
registerPromptRoutes(app, ctx);
|
|
46
51
|
registerPromptImportRoutes(app, ctx);
|
|
47
52
|
registerGrokRoutes(app, ctx);
|
|
53
|
+
registerAgyRoutes(app);
|
|
48
54
|
registerVideoRoutes(app, ctx);
|
|
49
55
|
registerVideoExtendedRoutes(app, ctx);
|
|
50
56
|
registerQuotaRoutes(app, ctx);
|
|
57
|
+
registerAuthRoutes(app);
|
|
58
|
+
mountKeyRoutes(app, ctx);
|
|
51
59
|
}
|