ima2-gen 2.0.15 → 2.0.16

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 (125) hide show
  1. package/README.md +30 -0
  2. package/bin/commands/skill.js +336 -17
  3. package/bin/ima2.js +22 -2
  4. package/bin/lib/doctor-checks.js +6 -4
  5. package/config.js +1 -0
  6. package/docs/CLI.md +25 -3
  7. package/docs/PROMPT_STUDIO.ko.md +29 -0
  8. package/docs/PROMPT_STUDIO.md +31 -0
  9. package/docs/migration/runtime-test-inventory.md +7 -1
  10. package/lib/agentImageVideoGen.js +8 -0
  11. package/lib/agentQueueStore.js +36 -10
  12. package/lib/agentQueueWorker.js +79 -7
  13. package/lib/agentRuntime.js +2 -0
  14. package/lib/assetLifecycle.js +40 -8
  15. package/lib/canvasVersionStore.js +86 -0
  16. package/lib/cardNewsGenerator.js +37 -3
  17. package/lib/cardNewsJobStore.js +10 -0
  18. package/lib/cardNewsManifestStore.js +6 -12
  19. package/lib/cardNewsPath.js +25 -0
  20. package/lib/db.js +2 -0
  21. package/lib/generatePipeline.js +474 -0
  22. package/lib/generationInputValidation.js +32 -0
  23. package/lib/grokImageCore.js +18 -1
  24. package/lib/historyIndex.js +11 -4
  25. package/lib/historyList.js +3 -0
  26. package/lib/imageMetadata.js +3 -0
  27. package/lib/inflight.js +30 -3
  28. package/lib/multimodePipeline.js +462 -0
  29. package/lib/nodeGeneration.js +440 -0
  30. package/lib/nodeValidation.js +23 -0
  31. package/lib/oauthProxy/generators.js +3 -246
  32. package/lib/oauthProxy/multimodeGenerators.js +251 -0
  33. package/lib/routeHelpers.js +9 -2
  34. package/package.json +2 -2
  35. package/routes/agent.js +4 -4
  36. package/routes/canvasVersions.js +21 -1
  37. package/routes/cardNews.js +2 -1
  38. package/routes/edit.js +25 -9
  39. package/routes/events.js +5 -1
  40. package/routes/generate.js +2 -481
  41. package/routes/keys.js +31 -39
  42. package/routes/multimode.js +2 -502
  43. package/routes/nodes.js +3 -448
  44. package/routes/video.js +12 -3
  45. package/routes/videoExtended.js +19 -4
  46. package/server.js +53 -1
  47. package/skills/ima2/SKILL.md +204 -0
  48. package/skills/ima2-front/SKILL.md +611 -0
  49. package/skills/ima2-front/references/a11y-patterns.md +134 -0
  50. package/skills/ima2-front/references/aesthetics.md +327 -0
  51. package/skills/ima2-front/references/anti-slop.md +241 -0
  52. package/skills/ima2-front/references/asset-requirements.md +431 -0
  53. package/skills/ima2-front/references/brand-asset-sourcing.md +122 -0
  54. package/skills/ima2-front/references/color-system.md +132 -0
  55. package/skills/ima2-front/references/consistency-locks.md +20 -0
  56. package/skills/ima2-front/references/crud-ui.md +53 -0
  57. package/skills/ima2-front/references/dropdown-layer.md +110 -0
  58. package/skills/ima2-front/references/i18n-global.md +104 -0
  59. package/skills/ima2-front/references/iterative-design.md +90 -0
  60. package/skills/ima2-front/references/korea-2026.md +174 -0
  61. package/skills/ima2-front/references/layout-discipline.md +161 -0
  62. package/skills/ima2-front/references/liquid-glass.md +181 -0
  63. package/skills/ima2-front/references/logo-sections.md +194 -0
  64. package/skills/ima2-front/references/mobile-ux.md +144 -0
  65. package/skills/ima2-front/references/performance-budget.md +106 -0
  66. package/skills/ima2-front/references/preflight-full.md +58 -0
  67. package/skills/ima2-front/references/product-density.md +53 -0
  68. package/skills/ima2-front/references/prototype-variants.md +54 -0
  69. package/skills/ima2-front/references/reference-capture.md +120 -0
  70. package/skills/ima2-front/references/responsive-viewport.md +139 -0
  71. package/skills/ima2-front/references/seo-baseline.md +123 -0
  72. package/skills/ima2-front/references/soft-3d-asset-gates.md +70 -0
  73. package/skills/ima2-front/references/stacks/astro.md +204 -0
  74. package/skills/ima2-front/references/stacks/mobile-native.md +281 -0
  75. package/skills/ima2-front/references/stacks/nextjs.md +219 -0
  76. package/skills/ima2-front/references/stacks/react.md +264 -0
  77. package/skills/ima2-front/references/stacks/svelte.md +235 -0
  78. package/skills/ima2-front/references/stacks/vanilla.md +226 -0
  79. package/skills/ima2-front/references/theme-switching.md +82 -0
  80. package/skills/ima2-front/references/top-bar.md +126 -0
  81. package/skills/ima2-front/references/typography-wrapping.md +270 -0
  82. package/skills/ima2-front/references/ux-writing-ko.md +62 -0
  83. package/skills/ima2-front/references/visual-verification.md +102 -0
  84. package/skills/ima2-uiux/SKILL.md +477 -0
  85. package/skills/ima2-uiux/references/anti-slop-judgment.md +39 -0
  86. package/skills/ima2-uiux/references/color-system.md +80 -0
  87. package/skills/ima2-uiux/references/design-isms.md +194 -0
  88. package/skills/ima2-uiux/references/design-read-example.md +33 -0
  89. package/skills/ima2-uiux/references/design-system-bootstrap.md +133 -0
  90. package/skills/ima2-uiux/references/favicon-logo.md +355 -0
  91. package/skills/ima2-uiux/references/form-patterns.md +118 -0
  92. package/skills/ima2-uiux/references/intent-discovery-ladder.md +86 -0
  93. package/skills/ima2-uiux/references/korean-design-vocabulary.md +52 -0
  94. package/skills/ima2-uiux/references/layout-macrostructures.md +54 -0
  95. package/skills/ima2-uiux/references/logo-trust-sections.md +96 -0
  96. package/skills/ima2-uiux/references/mobile-native-ux.md +207 -0
  97. package/skills/ima2-uiux/references/product-personalities.md +118 -0
  98. package/skills/ima2-uiux/references/responsive-nav.md +66 -0
  99. package/skills/ima2-uiux/references/typography-line-breaks.md +193 -0
  100. package/skills/ima2-uiux/references/ux-preflight.md +65 -0
  101. package/skills/ima2-uiux/references/ux-states.md +197 -0
  102. package/skills/ima2-uiux/references/visual-hierarchy.md +115 -0
  103. package/ui/dist/.vite/manifest.json +13 -13
  104. package/ui/dist/assets/AgentWorkspace-CtYt4SF5.js +3 -0
  105. package/ui/dist/assets/{CardNewsWorkspace-Cdxn1ohU.js → CardNewsWorkspace-DN5cVqG5.js} +1 -1
  106. package/ui/dist/assets/GenerationRequestLogPanel-DnmUQdKR.js +1 -0
  107. package/ui/dist/assets/{NodeCanvas-C67sLboA.js → NodeCanvas-CxpfIBI0.js} +1 -1
  108. package/ui/dist/assets/PromptBuilderPanel-C2R3ObtR.js +2 -0
  109. package/ui/dist/assets/{PromptImportDialog-DeQXsEEv.js → PromptImportDialog-BIxz_yEx.js} +2 -2
  110. package/ui/dist/assets/PromptImportDiscoverySection-BQTlPMME.js +1 -0
  111. package/ui/dist/assets/PromptImportFolderSection-Ds_Th9O3.js +1 -0
  112. package/ui/dist/assets/{PromptLibraryPanel-Db3xEPM_.js → PromptLibraryPanel-Rs4l45pV.js} +2 -2
  113. package/ui/dist/assets/{SettingsWorkspace-BDk_1W-V.js → SettingsWorkspace-p1AZ6uT9.js} +1 -1
  114. package/ui/dist/assets/index-0-_vgFGs.js +4 -0
  115. package/ui/dist/assets/index-Dm3pFxV4.js +23 -0
  116. package/ui/dist/assets/index-J8yDF3Ch.css +1 -0
  117. package/ui/dist/index.html +2 -2
  118. package/ui/dist/assets/AgentWorkspace-DAg_7zpY.js +0 -3
  119. package/ui/dist/assets/GenerationRequestLogPanel-C_qvEUi2.js +0 -1
  120. package/ui/dist/assets/PromptBuilderPanel-DjvVdAie.js +0 -2
  121. package/ui/dist/assets/PromptImportDiscoverySection-CrwyUKYI.js +0 -1
  122. package/ui/dist/assets/PromptImportFolderSection-CToGMqYE.js +0 -1
  123. package/ui/dist/assets/index-2oG6in1i.css +0 -1
  124. package/ui/dist/assets/index-CoxMOXAU.js +0 -23
  125. package/ui/dist/assets/index-p8egnGHk.js +0 -4
package/routes/edit.js CHANGED
@@ -11,7 +11,7 @@ import { editViaResponses } from "../lib/responsesImageAdapter.js";
11
11
  import { editViaGrok } from "../lib/grokImageAdapter.js";
12
12
  import { generateViaAgy } from "../lib/agyImageAdapter.js";
13
13
  import { generateViaGeminiApi } from "../lib/geminiApiImageAdapter.js";
14
- import { startJob, finishJob, registerJobAbortController, isJobCanceled } from "../lib/inflight.js";
14
+ import { startJob, finishJob, registerJobAbortController, isJobCanceled, isStartJobFailure, INFLIGHT_RETRY_AFTER_SECONDS } from "../lib/inflight.js";
15
15
  import { isGenerationCanceledError, makeGenerationCanceledError, throwIfJobCanceled, } from "../lib/generationCancel.js";
16
16
  import { logEvent, logError } from "../lib/logger.js";
17
17
  import { hasPngAlphaChannel, parsePngInfo } from "../lib/pngInfo.js";
@@ -84,6 +84,7 @@ export function registerEditRoutes(app, ctxRaw) {
84
84
  let finishErrorCode;
85
85
  let finishMeta = {};
86
86
  let finishCanceled = false;
87
+ let jobOwned = false;
87
88
  const cancelController = new AbortController();
88
89
  try {
89
90
  const { prompt, image: imageB64, mask: rawMask, quality: rawQuality = "medium", size = "1024x1024", moderation = "low", provider = "oauth", mode: promptMode = "auto", model: rawModel, reasoningEffort: rawReasoningEffort, webSearchEnabled: rawWebSearchEnabled = true, } = req.body;
@@ -108,7 +109,7 @@ export function registerEditRoutes(app, ctxRaw) {
108
109
  const activeProvider = providerOptions.provider;
109
110
  const sessionId = typeof req.body?.sessionId === "string" ? req.body.sessionId : null;
110
111
  const normalizedPromptMode = promptMode === "direct" ? "direct" : "auto";
111
- startJob({
112
+ const started = startJob({
112
113
  requestId,
113
114
  kind: "classic",
114
115
  prompt,
@@ -120,6 +121,20 @@ export function registerEditRoutes(app, ctxRaw) {
120
121
  size: effectiveSize,
121
122
  },
122
123
  });
124
+ if (started && isStartJobFailure(started)) {
125
+ const status = started.code === "TOO_MANY_JOBS" ? 429 : 409;
126
+ if (started.code === "TOO_MANY_JOBS") {
127
+ res.setHeader("Retry-After", String(INFLIGHT_RETRY_AFTER_SECONDS));
128
+ }
129
+ return res.status(status).json({
130
+ error: started.code === "TOO_MANY_JOBS"
131
+ ? "Too many concurrent generation jobs"
132
+ : "Request ID already in use",
133
+ code: started.code,
134
+ requestId,
135
+ });
136
+ }
137
+ jobOwned = true;
123
138
  registerJobAbortController(requestId, cancelController);
124
139
  if (!prompt || !imageB64) {
125
140
  finishStatus = "error";
@@ -335,13 +350,14 @@ export function registerEditRoutes(app, ctxRaw) {
335
350
  });
336
351
  }
337
352
  finally {
338
- finishJob(requestId, {
339
- canceled: finishCanceled,
340
- status: finishStatus,
341
- httpStatus: finishHttpStatus,
342
- errorCode: finishErrorCode,
343
- meta: finishMeta,
344
- });
353
+ if (jobOwned)
354
+ finishJob(requestId, {
355
+ canceled: finishCanceled,
356
+ status: finishStatus,
357
+ httpStatus: finishHttpStatus,
358
+ errorCode: finishErrorCode,
359
+ meta: finishMeta,
360
+ });
345
361
  }
346
362
  });
347
363
  }
package/routes/events.js CHANGED
@@ -14,6 +14,10 @@ function safeWrite(res, chunk) {
14
14
  function formatSse(ev) {
15
15
  return `id: ${ev.id}\nevent: ${ev.event}\ndata: ${JSON.stringify({ ...ev.data, jobId: ev.jobId })}\n\n`;
16
16
  }
17
+ function parseLastEventIdHeader(value) {
18
+ const raw = Array.isArray(value) ? value[0] : value;
19
+ return parseInt(raw?.split(",", 1)[0]?.trim() ?? "", 10);
20
+ }
17
21
  export function registerEventsRoute(app, _ctx) {
18
22
  app.get("/api/events", (req, res) => {
19
23
  if (activeConnections >= MAX_SSE_LISTENERS) {
@@ -27,7 +31,7 @@ export function registerEventsRoute(app, _ctx) {
27
31
  res.setHeader("X-Accel-Buffering", "no");
28
32
  res.flushHeaders?.();
29
33
  activeConnections++;
30
- const headerLastId = parseInt(req.headers["last-event-id"], 10);
34
+ const headerLastId = parseLastEventIdHeader(req.headers["last-event-id"]);
31
35
  const queryLastId = parseInt(String(req.query.lastEventId ?? ""), 10);
32
36
  const lastId = !Number.isNaN(headerLastId) ? headerLastId : queryLastId;
33
37
  if (!Number.isNaN(lastId)) {
@@ -1,485 +1,6 @@
1
- import { mkdir, readFile, writeFile } from "fs/promises";
2
- import { safeWriteSidecar, atomicWriteJson } from "../lib/atomicWrite.js";
3
- import { join } from "path";
4
- import { randomBytes } from "crypto";
5
- import { detectImageMimeFromB64, summarizeReferencePayload, validateAndNormalizeRefs } from "../lib/refs.js";
6
- import { generateImageThumbnailFromBuffer } from "../lib/imageThumb.js";
7
- import { classifyUpstreamError } from "../lib/errorClassify.js";
8
- import { appendGenerationRequestLog } from "../lib/generationRequestLog.js";
9
- import { normalizeOAuthParams } from "../lib/oauthNormalize.js";
10
- import { resolveProviderOptions } from "../lib/providerOptions.js";
11
- import { generateViaResponses } from "../lib/responsesImageAdapter.js";
12
- import { generateViaGrok, planGrokImage } from "../lib/grokImageAdapter.js";
13
- import { generateViaAgy } from "../lib/agyImageAdapter.js";
14
- import { generateViaGeminiApi } from "../lib/geminiApiImageAdapter.js";
15
- import { isNonRetryableGenerationError, normalizeGenerationFailure } from "../lib/generationErrors.js";
16
- import { startJob, finishJob, registerJobAbortController, isJobCanceled, isStartJobFailure, setJobPhase, INFLIGHT_RETRY_AFTER_SECONDS, } from "../lib/inflight.js";
17
- import { isGenerationCanceledError, makeGenerationCanceledError, throwIfJobCanceled, } from "../lib/generationCancel.js";
18
- import { logEvent, logError } from "../lib/logger.js";
19
- import { embedImageMetadataBestEffort } from "../lib/imageMetadataStore.js";
20
- import { invalidateHistoryIndex } from "../lib/historyIndex.js";
21
- import { normalizeComposerInsertedPrompts, normalizeComposerPrompt, } from "../lib/composerSnapshot.js";
22
- import { errInfo } from "../lib/errInfo.js";
1
+ import { runGeneratePipeline } from "../lib/generatePipeline.js";
23
2
  import { requireRuntimeContext } from "../lib/runtimeContext.js";
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";
28
3
  export function registerGenerateRoutes(app, ctxRaw) {
29
4
  const ctx = requireRuntimeContext(ctxRaw);
30
- app.post("/api/generate", async (req, res) => {
31
- const requestId = typeof req.body?.requestId === "string" ? req.body.requestId : req.id;
32
- const asyncMode = req.body?.async === true;
33
- let finishStatus = "completed";
34
- let finishHttpStatus;
35
- let finishErrorCode;
36
- let finishMeta = {};
37
- let finishCanceled = false;
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
- };
56
- try {
57
- const sessionId = typeof req.body?.sessionId === "string" ? req.body.sessionId : null;
58
- const clientNodeId = typeof req.body?.clientNodeId === "string" ? req.body.clientNodeId : null;
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;
60
- const storyboardActive = req.body?.storyboard === true;
61
- const storyboardPrefix = storyboardActive ? STORYBOARD_PREFIX : "";
62
- const composerPrompt = normalizeComposerPrompt(req.body?.composerPrompt);
63
- const composerInsertedPrompts = normalizeComposerInsertedPrompts(req.body?.composerInsertedPrompts);
64
- const { quality, warnings: qualityWarnings } = normalizeOAuthParams({ provider, quality: rawQuality });
65
- const providerOptions = resolveProviderOptions(ctx, {
66
- provider,
67
- rawModel,
68
- rawReasoningEffort,
69
- rawSize: size,
70
- rawWebSearchEnabled,
71
- });
72
- if (providerOptions.error) {
73
- return fail(providerOptions.status, { error: providerOptions.error, code: providerOptions.code });
74
- }
75
- const imageModel = providerOptions.model;
76
- const reasoningEffort = providerOptions.reasoningEffort;
77
- const effectiveSize = providerOptions.size;
78
- const webSearchEnabled = providerOptions.webSearchEnabled;
79
- const activeProvider = providerOptions.provider;
80
- const normalizedPromptMode = promptMode === "direct" ? "direct" : "auto";
81
- const generationPrompt = storyboardPrefix + prompt;
82
- if (!prompt)
83
- return fail(400, { error: "Prompt is required" });
84
- const moderationCheck = validateModeration(ctx, moderation);
85
- if (moderationCheck.error)
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);
89
- const referencePayload = summarizeReferencePayload(references);
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({
112
- requestId,
113
- kind: "classic",
114
- prompt,
115
- meta: {
116
- kind: "classic",
117
- sessionId,
118
- parentNodeId: null,
119
- clientNodeId,
120
- quality,
121
- model: imageModel,
122
- size: effectiveSize,
123
- n: count,
124
- refsCount: providerRefCount,
125
- referenceBytes: referencePayload.referenceBytes,
126
- referenceB64Chars: referencePayload.referenceB64Chars,
127
- composerPrompt,
128
- composerInsertedPrompts,
129
- },
130
- });
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,
141
- requestId,
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" });
151
- const client = req.get("x-ima2-client") || "ui";
152
- const referenceDiagnostics = refCheck.referenceDiagnostics || [];
153
- const referenceMismatchCount = referenceDiagnostics.filter((ref) => ref.warnings?.includes("mime_mismatch")).length;
154
- logEvent("generate", "request", {
155
- requestId,
156
- client,
157
- provider: activeProvider,
158
- quality,
159
- model: imageModel,
160
- size: effectiveSize,
161
- moderation,
162
- n: count,
163
- refs: providerRefCount,
164
- referenceBytes: referencePayload.referenceBytes,
165
- referenceMismatchCount,
166
- refDetectedMimes: [...new Set(referenceDiagnostics.map((ref) => ref.detectedMime).filter(Boolean))].join(","),
167
- refDeclaredMimes: [...new Set(referenceDiagnostics.map((ref) => ref.declaredMime).filter(Boolean))].join(","),
168
- sessionId,
169
- clientNodeId,
170
- promptChars: typeof prompt === "string" ? prompt.length : 0,
171
- promptMode: normalizedPromptMode,
172
- webSearchEnabled,
173
- });
174
- const startTime = Date.now();
175
- const mimeMap = { png: "image/png", jpeg: "image/jpeg", webp: "image/webp" };
176
- const effectiveFormat = activeProvider === "grok" || activeProvider === "agy" || activeProvider === "grok-api" || activeProvider === "gemini-api" ? "jpeg" : String(format);
177
- const mime = mimeMap[effectiveFormat] || "image/png";
178
- await mkdir(ctx.config.storage.generatedDir, { recursive: true });
179
- const grokDirectApiKey = activeProvider === "grok-api" ? ctx.xaiApiKey : undefined;
180
- const sharedGrokPlan = activeProvider === "grok" || activeProvider === "grok-api"
181
- ? await planGrokImage(generationPrompt, ctx, {
182
- model: quality === "high" ? "grok-imagine-image-quality" : imageModel,
183
- size: effectiveSize,
184
- signal: cancelController.signal,
185
- requestId,
186
- referenceCount: grokRefs.length,
187
- references: grokRefs,
188
- directApiKey: grokDirectApiKey,
189
- })
190
- : null;
191
- const generateOne = async () => {
192
- if (activeProvider === "gemini-api") {
193
- const r = await generateViaGeminiApi(generationPrompt, requireRuntimeContext(ctx), {
194
- model: imageModel,
195
- size: effectiveSize,
196
- signal: cancelController.signal,
197
- requestId,
198
- references: refCheck.refDetails,
199
- });
200
- throwIfJobCanceled(requestId);
201
- return r;
202
- }
203
- if (activeProvider === "agy") {
204
- const r = await generateViaAgy(generationPrompt, {
205
- references: refCheck.refDetails,
206
- signal: cancelController.signal,
207
- requestId,
208
- });
209
- throwIfJobCanceled(requestId);
210
- return r;
211
- }
212
- if (activeProvider === "grok" || activeProvider === "grok-api") {
213
- const grokModel = quality === "high" ? "grok-imagine-image-quality" : imageModel;
214
- const r = await generateViaGrok(generationPrompt, ctx, {
215
- model: grokModel,
216
- size: effectiveSize,
217
- signal: cancelController.signal,
218
- requestId,
219
- plannedPrompt: sharedGrokPlan?.prompt,
220
- webSearchCalls: sharedGrokPlan?.webSearchCalls,
221
- references: grokRefs,
222
- directApiKey: grokDirectApiKey,
223
- });
224
- throwIfJobCanceled(requestId);
225
- return r;
226
- }
227
- const MAX_RETRIES = 1;
228
- let lastErr;
229
- for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
230
- try {
231
- const r = await generateViaResponses(activeProvider, generationPrompt, quality, effectiveSize, moderation, refCheck.refDetails || refCheck.refs, requestId, normalizedPromptMode, ctx, {
232
- model: imageModel,
233
- reasoningEffort,
234
- webSearchEnabled,
235
- signal: cancelController.signal,
236
- allowPromptOnlyOAuthFallback: activeProvider !== "api",
237
- });
238
- throwIfJobCanceled(requestId);
239
- if (r.b64)
240
- return r;
241
- lastErr = new Error("Empty response (safety refusal)");
242
- }
243
- catch (e) {
244
- lastErr = e;
245
- if (isNonRetryableGenerationError(e))
246
- break;
247
- }
248
- if (attempt < MAX_RETRIES) {
249
- const errCode = (lastErr && typeof lastErr === "object" && "code" in lastErr)
250
- ? lastErr.code
251
- : undefined;
252
- logEvent("generate", "retry", { requestId, attempt: attempt + 1, errorCode: errCode });
253
- }
254
- }
255
- throw normalizeGenerationFailure(lastErr, {
256
- safetyMessage: "Content generation refused after retries",
257
- });
258
- };
259
- const results = await Promise.allSettled(Array.from({ length: count }, generateOne));
260
- throwIfJobCanceled(requestId);
261
- const images = [];
262
- let totalUsage = null;
263
- let totalWebSearchCalls = 0;
264
- let firstRetryMeta = null;
265
- for (const r of results) {
266
- if (r.status === "fulfilled" && r.value.b64) {
267
- throwIfJobCanceled(requestId);
268
- const valueWithMime = r.value;
269
- const resultMime = activeProvider === "grok" || activeProvider === "agy" || activeProvider === "grok-api" || activeProvider === "gemini-api"
270
- ? (valueWithMime.mime || detectImageMimeFromB64(r.value.b64) || mime)
271
- : mime;
272
- const resultFormat = activeProvider === "grok" || activeProvider === "agy" || activeProvider === "grok-api" || activeProvider === "gemini-api" ? imageFormatFromMime(resultMime) : effectiveFormat;
273
- const retryValue = r.value;
274
- if (!firstRetryMeta && retryValue.retryKind) {
275
- firstRetryMeta = {
276
- retryKind: retryValue.retryKind,
277
- initialEventCount: retryValue.initialEventCount ?? null,
278
- initialEventTypes: retryValue.initialEventTypes || null,
279
- referencesDroppedOnRetry: retryValue.referencesDroppedOnRetry ?? null,
280
- developerPromptDroppedOnRetry: retryValue.developerPromptDroppedOnRetry ?? null,
281
- webSearchDroppedOnRetry: retryValue.webSearchDroppedOnRetry ?? null,
282
- };
283
- }
284
- const rand = randomBytes(ctx.config.ids.generatedHexBytes).toString("hex");
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;
291
- const meta = {
292
- kind: "classic",
293
- requestId,
294
- sessionId,
295
- clientNodeId,
296
- prompt,
297
- userPrompt: prompt,
298
- revisedPrompt: r.value.revisedPrompt || null,
299
- promptMode: normalizedPromptMode,
300
- composerPrompt,
301
- composerInsertedPrompts,
302
- quality,
303
- size: effectiveSize,
304
- format: resultFormat,
305
- moderation,
306
- model: activeProvider === "grok" ? (quality === "high" ? "grok-imagine-image-quality" : imageModel) : imageModel,
307
- reasoningEffort,
308
- provider: activeProvider,
309
- createdAt,
310
- ...(providerUrl ? { providerUrl } : {}),
311
- usage: r.value.usage || null,
312
- webSearchCalls: r.value.webSearchCalls || 0,
313
- webSearchEnabled,
314
- refsCount: providerRefCount,
315
- };
316
- const rawBuffer = Buffer.from(r.value.b64, "base64");
317
- const embedded = await embedImageMetadataBestEffort(rawBuffer, resultFormat, meta, {
318
- version: ctx.packageVersion,
319
- });
320
- if (!embedded.embedded) {
321
- logEvent("generate", "metadata_embed_skipped", {
322
- requestId,
323
- filename,
324
- code: embedded.code,
325
- warning: embedded.warning,
326
- });
327
- }
328
- const filePath = join(ctx.config.storage.generatedDir, filename);
329
- await writeFile(filePath, embedded.buffer);
330
- await safeWriteSidecar(filePath + ".json", meta);
331
- generateImageThumbnailFromBuffer(embedded.buffer, filePath).catch(() => { });
332
- invalidateHistoryIndex();
333
- images.push({
334
- image: `data:${resultMime};base64,${r.value.b64}`,
335
- filename,
336
- revisedPrompt: r.value.revisedPrompt || null,
337
- ...(providerUrl ? { providerUrl } : {}),
338
- createdAt,
339
- });
340
- if (r.value.usage) {
341
- const usageValue = r.value.usage;
342
- if (!totalUsage)
343
- totalUsage = { ...usageValue };
344
- else {
345
- const tu = totalUsage;
346
- Object.keys(usageValue).forEach((k) => {
347
- if (typeof usageValue[k] === "number")
348
- tu[k] = (tu[k] || 0) + usageValue[k];
349
- });
350
- }
351
- }
352
- if (typeof r.value.webSearchCalls === "number") {
353
- totalWebSearchCalls = activeProvider === "grok" || activeProvider === "grok-api"
354
- ? Math.max(totalWebSearchCalls, r.value.webSearchCalls)
355
- : totalWebSearchCalls + r.value.webSearchCalls;
356
- }
357
- }
358
- else if (r.status === "rejected") {
359
- logError("generate", "parallel_failed", r.reason, { requestId });
360
- }
361
- }
362
- if (images.length === 0) {
363
- const firstErr = results.find((r) => r.status === "rejected")?.reason;
364
- if (firstErr?.code) {
365
- const status = firstErr.status || 500;
366
- if (isGenerationCanceledError(firstErr)) {
367
- finishCanceled = true;
368
- return fail(firstErr.status, {
369
- error: firstErr.message,
370
- code: firstErr.code,
371
- requestId,
372
- });
373
- }
374
- return fail(status, {
375
- error: firstErr.message,
376
- code: firstErr.code,
377
- ...upstreamErrorFields(firstErr),
378
- requestId,
379
- });
380
- }
381
- return fail(500, { error: "All generation attempts failed", code: "GENERATE_ALL_FAILED", requestId });
382
- }
383
- const elapsed = +((Date.now() - startTime) / 1000).toFixed(1);
384
- // Persist elapsed (computed after the generation loop) into each image's sidecar.
385
- // forward-fix: only newly generated items get elapsed. The embedded XMP is written
386
- // earlier in the loop (before elapsed exists), so history reload relies on this sidecar patch.
387
- await Promise.all(images.map(async ({ filename }) => {
388
- try {
389
- const sidecarPath = join(ctx.config.storage.generatedDir, filename + ".json");
390
- const sidecarMeta = JSON.parse(await readFile(sidecarPath, "utf-8"));
391
- sidecarMeta.elapsed = elapsed;
392
- await atomicWriteJson(sidecarPath, sidecarMeta);
393
- }
394
- catch {
395
- /* best-effort elapsed patch */
396
- }
397
- }));
398
- const firstRevised = images[0]?.revisedPrompt || null;
399
- const extra = {
400
- usage: totalUsage,
401
- provider: activeProvider,
402
- reasoningEffort,
403
- webSearchCalls: totalWebSearchCalls,
404
- quality,
405
- size: effectiveSize,
406
- moderation,
407
- model: imageModel,
408
- warnings: qualityWarnings,
409
- revisedPrompt: firstRevised,
410
- promptMode: normalizedPromptMode,
411
- webSearchEnabled,
412
- ...(firstRetryMeta || {}),
413
- };
414
- if (count === 1) {
415
- finishHttpStatus = 200;
416
- finishMeta = { filenames: [images[0].filename], imageCount: 1 };
417
- logEvent("generate", "saved", {
418
- requestId,
419
- imageCount: 1,
420
- elapsedMs: Date.now() - startTime,
421
- filename: images[0].filename,
422
- });
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
- });
432
- }
433
- else {
434
- finishHttpStatus = 200;
435
- finishMeta = { filenames: images.map((image) => image.filename), imageCount: images.length };
436
- logEvent("generate", "saved", {
437
- requestId,
438
- imageCount: images.length,
439
- elapsedMs: Date.now() - startTime,
440
- });
441
- succeed({ images, elapsed, count: images.length, requestId, ...extra });
442
- }
443
- }
444
- catch (e) {
445
- const err = errInfo(e);
446
- const ext = (err.raw && typeof err.raw === "object" ? err.raw : {});
447
- const fallbackCode = err.code || classifyUpstreamError(err.message);
448
- if (isGenerationCanceledError(err.raw) || isJobCanceled(requestId)) {
449
- const canceled = makeGenerationCanceledError();
450
- finishCanceled = true;
451
- return fail(canceled.status, {
452
- error: canceled.message,
453
- code: canceled.code,
454
- requestId,
455
- });
456
- }
457
- finishErrorCode = fallbackCode || "GENERATE_FAILED";
458
- logError("generate", "error", err.raw, { requestId, code: finishErrorCode });
459
- fail(err.status || 500, {
460
- error: err.message,
461
- code: finishErrorCode,
462
- ...upstreamErrorFields(ext),
463
- requestId,
464
- });
465
- }
466
- finally {
467
- finishJob(requestId, {
468
- canceled: finishCanceled,
469
- status: finishStatus,
470
- httpStatus: finishHttpStatus,
471
- errorCode: finishErrorCode,
472
- meta: finishMeta,
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(() => { });
483
- }
484
- });
5
+ app.post("/api/generate", (req, res) => runGeneratePipeline(req, res, ctx));
485
6
  }