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.
Files changed (87) hide show
  1. package/CHANGELOG.md +150 -0
  2. package/README.md +12 -12
  3. package/bin/commands/backfillThumbs.js +24 -0
  4. package/bin/commands/edit.js +7 -6
  5. package/bin/commands/gen.js +13 -6
  6. package/bin/commands/multimode.js +5 -4
  7. package/bin/commands/node.js +4 -4
  8. package/bin/ima2.js +21 -11
  9. package/bin/lib/config-store.js +1 -1
  10. package/docs/API.md +184 -10
  11. package/docs/CLI.md +11 -4
  12. package/docs/FAQ.ko.md +16 -0
  13. package/docs/FAQ.md +30 -0
  14. package/docs/PROMPT_STUDIO.md +3 -1
  15. package/docs/README.ko.md +7 -3
  16. package/docs/migration/runtime-test-inventory.md +17 -1
  17. package/lib/agentImageVideoGen.js +261 -0
  18. package/lib/agentRuntime.js +11 -260
  19. package/lib/agentSettings.js +1 -1
  20. package/lib/agyImageAdapter.js +259 -0
  21. package/lib/capabilities.js +2 -1
  22. package/lib/configKeys.js +1 -1
  23. package/lib/errorClassify.js +8 -7
  24. package/lib/eventBus.js +71 -0
  25. package/lib/geminiApiImageAdapter.js +179 -0
  26. package/lib/generationErrors.js +3 -1
  27. package/lib/grokImageAdapter.js +74 -128
  28. package/lib/grokImageCore.js +153 -0
  29. package/lib/grokMultimodeAdapter.js +7 -4
  30. package/lib/grokRuntime.js +3 -0
  31. package/lib/grokSizeMapper.js +13 -1
  32. package/lib/grokVideoAdapter.js +14 -7
  33. package/lib/grokVideoCanvas.js +13 -0
  34. package/lib/grokVideoPlannerPrompt.js +53 -6
  35. package/lib/historyList.js +19 -2
  36. package/lib/imageModels.js +15 -0
  37. package/lib/imageThumb.js +38 -0
  38. package/lib/inflight.js +54 -17
  39. package/lib/multimodeHelpers.js +10 -0
  40. package/lib/nodeHelpers.js +59 -0
  41. package/lib/oauthProxy/prompts.js +30 -36
  42. package/lib/promptBuilder/systemPrompt.js +2 -5
  43. package/lib/promptSafetyPolicy.js +1 -5
  44. package/lib/providerOptions.js +36 -1
  45. package/lib/responsesFallback.js +53 -44
  46. package/lib/routeHelpers.js +44 -0
  47. package/lib/runtimeContext.js +27 -0
  48. package/lib/ssePublish.js +12 -0
  49. package/lib/storageMigration.js +1 -1
  50. package/lib/storyboardPrefix.js +28 -0
  51. package/lib/thumbBackfill.js +70 -0
  52. package/lib/vertexAuth.js +44 -0
  53. package/lib/videoThumb.js +60 -0
  54. package/package.json +7 -2
  55. package/routes/agy.js +44 -0
  56. package/routes/auth.js +242 -0
  57. package/routes/edit.js +48 -8
  58. package/routes/events.js +78 -0
  59. package/routes/generate.js +135 -135
  60. package/routes/history.js +13 -0
  61. package/routes/index.js +8 -0
  62. package/routes/keys.js +254 -0
  63. package/routes/multimode.js +138 -62
  64. package/routes/nodes.js +107 -129
  65. package/routes/quota.js +58 -7
  66. package/routes/video.js +107 -20
  67. package/server.js +123 -0
  68. package/skills/ima2/SKILL.md +98 -21
  69. package/ui/dist/.vite/manifest.json +12 -12
  70. package/ui/dist/assets/AgentWorkspace-Dth6YijN.js +3 -0
  71. package/ui/dist/assets/{CardNewsWorkspace-BN-ga1lG.js → CardNewsWorkspace-Dav3K5CT.js} +2 -2
  72. package/ui/dist/assets/{NodeCanvas-BbMa4IhI.js → NodeCanvas-C4ifFzB1.js} +2 -2
  73. package/ui/dist/assets/{PromptBuilderPanel-DRwBJRDQ.js → PromptBuilderPanel-CEcyU9PL.js} +1 -1
  74. package/ui/dist/assets/{PromptImportDialog-Dp85kHCq.js → PromptImportDialog-CgQ94Gth.js} +2 -2
  75. package/ui/dist/assets/{PromptImportDiscoverySection-BE8Q8MLD.js → PromptImportDiscoverySection-CuzyzbNI.js} +1 -1
  76. package/ui/dist/assets/{PromptImportFolderSection-PtH5x0sc.js → PromptImportFolderSection-DHLGlO6l.js} +1 -1
  77. package/ui/dist/assets/{PromptLibraryPanel-FnM9tHI9.js → PromptLibraryPanel-BOe18we8.js} +2 -2
  78. package/ui/dist/assets/SettingsWorkspace-Cdgnm4Wa.js +1 -0
  79. package/ui/dist/assets/index-C5PSahkr.js +1 -0
  80. package/ui/dist/assets/index-Dn2AhL6d.css +1 -0
  81. package/ui/dist/assets/index-Tjqx6wUV.js +23 -0
  82. package/ui/dist/index.html +2 -2
  83. package/ui/dist/assets/AgentWorkspace-C21zqdTZ.js +0 -3
  84. package/ui/dist/assets/SettingsWorkspace-MARPGyBL.js +0 -1
  85. package/ui/dist/assets/index-BAFI6htx.js +0 -42
  86. package/ui/dist/assets/index-BSXxr_Bt.js +0 -1
  87. package/ui/dist/assets/index-DS-ADE7U.css +0 -1
@@ -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
- function validateModeration(ctx, moderation) {
21
- if (typeof moderation !== "string" || !ctx.config.oauth.validModeration.has(moderation)) {
22
- return { error: "moderation must be one of: auto, low" };
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
- finishStatus = "error";
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 res.status(400).json({ error: "Prompt is required" });
82
+ return fail(400, { error: "Prompt is required" });
96
83
  const moderationCheck = validateModeration(ctx, moderation);
97
84
  if (moderationCheck.error)
98
- return res.status(400).json({ error: moderationCheck.error });
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
- startJob({
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: referencePayload.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
- registerJobAbortController(requestId, cancelController);
122
- const refCheckResult = validateAndNormalizeRefs(references);
123
- if (refCheckResult.error) {
124
- finishStatus = "error";
125
- finishHttpStatus = 400;
126
- finishErrorCode = refCheckResult.code;
127
- return res.status(400).json({ error: refCheckResult.error, code: refCheckResult.code });
128
- }
129
- const refCheck = refCheckResult;
130
- if (activeProvider === "grok" && refCheck.refs.length > 3) {
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: refCheck.refs.length,
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 sharedGrokPlan = activeProvider === "grok"
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: refCheck.refs.length,
175
- references: refCheck.refDetails,
184
+ referenceCount: grokRefs.length,
185
+ references: grokRefs,
186
+ directApiKey: grokDirectApiKey,
176
187
  })
177
188
  : null;
178
189
  const generateOne = async () => {
179
- if (activeProvider === "grok") {
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: refCheck.refDetails,
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: Date.now(),
307
+ createdAt,
308
+ ...(providerUrl ? { providerUrl } : {}),
271
309
  usage: r.value.usage || null,
272
310
  webSearchCalls: r.value.webSearchCalls || 0,
273
311
  webSearchEnabled,
274
- refsCount: refCheck.refs.length,
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
- await writeFile(join(ctx.config.storage.generatedDir, filename), embedded.buffer);
289
- await safeWriteSidecar(join(ctx.config.storage.generatedDir, filename + ".json"), meta);
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
- finishHttpStatus = firstErr.status;
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
- finishStatus = "error";
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
- upstreamCode: firstErr.upstreamCode || null,
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
- finishStatus = "error";
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
- res.json({ image: images[0].image, elapsed, filename: images[0].filename, requestId, ...extra });
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
- res.json({ images, elapsed, count: images.length, requestId, ...extra });
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
- finishHttpStatus = canceled.status;
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
- res.status(err.status || 500).json({
457
+ fail(err.status || 500, {
439
458
  error: err.message,
440
- code: fallbackCode,
441
- upstreamCode: ext.upstreamCode || null,
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
  }