ima2-gen 1.1.7 → 1.1.9

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 (229) hide show
  1. package/README.md +56 -27
  2. package/bin/commands/annotate.js +137 -0
  3. package/bin/commands/annotate.ts +118 -0
  4. package/bin/commands/cancel.js +37 -33
  5. package/bin/commands/cancel.ts +45 -0
  6. package/bin/commands/canvas-versions.js +91 -0
  7. package/bin/commands/canvas-versions.ts +80 -0
  8. package/bin/commands/cardnews.js +293 -0
  9. package/bin/commands/cardnews.ts +248 -0
  10. package/bin/commands/comfy.js +63 -0
  11. package/bin/commands/comfy.ts +54 -0
  12. package/bin/commands/config.js +270 -0
  13. package/bin/commands/config.ts +265 -0
  14. package/bin/commands/edit.js +97 -72
  15. package/bin/commands/edit.ts +116 -0
  16. package/bin/commands/gen.js +140 -118
  17. package/bin/commands/gen.ts +176 -0
  18. package/bin/commands/history.js +164 -0
  19. package/bin/commands/history.ts +145 -0
  20. package/bin/commands/ls.js +60 -42
  21. package/bin/commands/ls.ts +60 -0
  22. package/bin/commands/metadata.js +45 -0
  23. package/bin/commands/metadata.ts +36 -0
  24. package/bin/commands/multimode.js +159 -0
  25. package/bin/commands/multimode.ts +146 -0
  26. package/bin/commands/node.js +176 -0
  27. package/bin/commands/node.ts +157 -0
  28. package/bin/commands/observability.js +201 -0
  29. package/bin/commands/observability.ts +176 -0
  30. package/bin/commands/ping.js +26 -20
  31. package/bin/commands/ping.ts +29 -0
  32. package/bin/commands/prompt.js +506 -0
  33. package/bin/commands/prompt.ts +421 -0
  34. package/bin/commands/ps.js +78 -71
  35. package/bin/commands/ps.ts +78 -0
  36. package/bin/commands/session.js +308 -0
  37. package/bin/commands/session.ts +265 -0
  38. package/bin/commands/show.js +75 -40
  39. package/bin/commands/show.ts +69 -0
  40. package/bin/ima2.js +324 -310
  41. package/bin/ima2.ts +444 -0
  42. package/bin/lib/args.js +75 -66
  43. package/bin/lib/args.ts +73 -0
  44. package/bin/lib/browser-id.js +15 -0
  45. package/bin/lib/browser-id.ts +16 -0
  46. package/bin/lib/client.js +91 -83
  47. package/bin/lib/client.ts +109 -0
  48. package/bin/lib/error-hints.js +14 -17
  49. package/bin/lib/error-hints.ts +23 -0
  50. package/bin/lib/files.js +26 -28
  51. package/bin/lib/files.ts +39 -0
  52. package/bin/lib/output.js +44 -42
  53. package/bin/lib/output.ts +58 -0
  54. package/bin/lib/platform.js +60 -56
  55. package/bin/lib/platform.ts +97 -0
  56. package/bin/lib/sse.js +73 -0
  57. package/bin/lib/sse.ts +73 -0
  58. package/bin/lib/star-prompt.js +69 -76
  59. package/bin/lib/star-prompt.ts +97 -0
  60. package/bin/lib/storage-doctor.js +34 -35
  61. package/bin/lib/storage-doctor.ts +38 -0
  62. package/config.js +147 -190
  63. package/config.ts +331 -0
  64. package/docs/API.md +48 -8
  65. package/docs/CLI.md +190 -0
  66. package/docs/FAQ.ko.md +5 -5
  67. package/docs/FAQ.md +5 -5
  68. package/docs/README.ja.md +71 -25
  69. package/docs/README.ko.md +61 -24
  70. package/docs/README.zh-CN.md +73 -27
  71. package/lib/assetLifecycle.js +130 -130
  72. package/lib/assetLifecycle.ts +142 -0
  73. package/lib/canvasVersionStore.js +135 -153
  74. package/lib/canvasVersionStore.ts +181 -0
  75. package/lib/cardNewsGenerator.js +127 -142
  76. package/lib/cardNewsGenerator.ts +162 -0
  77. package/lib/cardNewsJobStore.js +78 -84
  78. package/lib/cardNewsJobStore.ts +107 -0
  79. package/lib/cardNewsManifestStore.js +88 -93
  80. package/lib/cardNewsManifestStore.ts +112 -0
  81. package/lib/cardNewsPlanner.js +157 -152
  82. package/lib/cardNewsPlanner.ts +180 -0
  83. package/lib/cardNewsPlannerClient.js +101 -98
  84. package/lib/cardNewsPlannerClient.ts +114 -0
  85. package/lib/cardNewsPlannerPrompt.js +56 -56
  86. package/lib/cardNewsPlannerPrompt.ts +60 -0
  87. package/lib/cardNewsPlannerSchema.js +231 -223
  88. package/lib/cardNewsPlannerSchema.ts +259 -0
  89. package/lib/cardNewsRoleTemplateStore.js +39 -41
  90. package/lib/cardNewsRoleTemplateStore.ts +47 -0
  91. package/lib/cardNewsTemplateStore.js +171 -175
  92. package/lib/cardNewsTemplateStore.ts +210 -0
  93. package/lib/codexDetect.js +44 -47
  94. package/lib/codexDetect.ts +69 -0
  95. package/lib/comfyBridge.js +164 -184
  96. package/lib/comfyBridge.ts +214 -0
  97. package/lib/db.js +41 -51
  98. package/lib/db.ts +166 -0
  99. package/lib/errorClassify.js +62 -78
  100. package/lib/errorClassify.ts +100 -0
  101. package/lib/generationErrors.js +140 -103
  102. package/lib/generationErrors.ts +125 -0
  103. package/lib/historyList.js +149 -147
  104. package/lib/historyList.ts +164 -0
  105. package/lib/imageMetadata.js +86 -89
  106. package/lib/imageMetadata.ts +111 -0
  107. package/lib/imageMetadataStore.js +46 -51
  108. package/lib/imageMetadataStore.ts +67 -0
  109. package/lib/imageModels.js +38 -45
  110. package/lib/imageModels.ts +52 -0
  111. package/lib/inflight.js +131 -150
  112. package/lib/inflight.ts +204 -0
  113. package/lib/localImportStore.js +105 -0
  114. package/lib/localImportStore.ts +111 -0
  115. package/lib/logger.js +105 -112
  116. package/lib/logger.ts +150 -0
  117. package/lib/nodeStore.js +65 -64
  118. package/lib/nodeStore.ts +81 -0
  119. package/lib/oauthLauncher.js +61 -59
  120. package/lib/oauthLauncher.ts +64 -0
  121. package/lib/oauthNormalize.js +15 -19
  122. package/lib/oauthNormalize.ts +30 -0
  123. package/lib/oauthProxy.js +834 -832
  124. package/lib/oauthProxy.ts +995 -0
  125. package/lib/openDirectory.js +41 -40
  126. package/lib/openDirectory.ts +45 -0
  127. package/lib/pngInfo.js +18 -20
  128. package/lib/pngInfo.ts +26 -0
  129. package/lib/promptImport/curatedSources.js +135 -0
  130. package/lib/promptImport/curatedSources.ts +139 -0
  131. package/lib/promptImport/discoveryRegistry.js +218 -0
  132. package/lib/promptImport/discoveryRegistry.ts +236 -0
  133. package/lib/promptImport/errors.js +10 -10
  134. package/lib/promptImport/errors.ts +18 -0
  135. package/lib/promptImport/githubDiscovery.js +238 -0
  136. package/lib/promptImport/githubDiscovery.ts +248 -0
  137. package/lib/promptImport/githubFolder.js +302 -0
  138. package/lib/promptImport/githubFolder.ts +308 -0
  139. package/lib/promptImport/githubSource.js +194 -171
  140. package/lib/promptImport/githubSource.ts +239 -0
  141. package/lib/promptImport/gptImageHints.js +61 -0
  142. package/lib/promptImport/gptImageHints.ts +68 -0
  143. package/lib/promptImport/parsePromptCandidates.js +110 -112
  144. package/lib/promptImport/parsePromptCandidates.ts +153 -0
  145. package/lib/promptImport/promptIndex.js +230 -0
  146. package/lib/promptImport/promptIndex.ts +248 -0
  147. package/lib/promptImport/rankPromptCandidates.js +52 -0
  148. package/lib/promptImport/rankPromptCandidates.ts +49 -0
  149. package/lib/providerOptions.js +31 -0
  150. package/lib/providerOptions.ts +41 -0
  151. package/lib/referenceImageCompress.js +51 -62
  152. package/lib/referenceImageCompress.ts +75 -0
  153. package/lib/refs.js +93 -81
  154. package/lib/refs.ts +117 -0
  155. package/lib/requestLogger.js +32 -38
  156. package/lib/requestLogger.ts +48 -0
  157. package/lib/responsesImageAdapter.js +351 -0
  158. package/lib/responsesImageAdapter.ts +352 -0
  159. package/lib/runtimePorts.js +71 -73
  160. package/lib/runtimePorts.ts +93 -0
  161. package/lib/sessionStore.js +179 -230
  162. package/lib/sessionStore.ts +272 -0
  163. package/lib/storageMigration.js +247 -245
  164. package/lib/storageMigration.ts +284 -0
  165. package/lib/styleSheet.js +86 -90
  166. package/lib/styleSheet.ts +128 -0
  167. package/lib/systemTrash.js +18 -0
  168. package/lib/systemTrash.ts +20 -0
  169. package/package.json +26 -10
  170. package/routes/annotations.js +76 -79
  171. package/routes/annotations.ts +95 -0
  172. package/routes/canvasVersions.js +50 -54
  173. package/routes/canvasVersions.ts +64 -0
  174. package/routes/cardNews.js +158 -171
  175. package/routes/cardNews.ts +183 -0
  176. package/routes/comfy.js +23 -31
  177. package/routes/comfy.ts +39 -0
  178. package/routes/edit.js +183 -214
  179. package/routes/edit.ts +230 -0
  180. package/routes/generate.js +269 -291
  181. package/routes/generate.ts +309 -0
  182. package/routes/health.js +102 -107
  183. package/routes/health.ts +114 -0
  184. package/routes/history.js +136 -144
  185. package/routes/history.ts +153 -0
  186. package/routes/imageImport.js +33 -0
  187. package/routes/imageImport.ts +33 -0
  188. package/routes/index.js +18 -16
  189. package/routes/index.ts +35 -0
  190. package/routes/metadata.js +60 -64
  191. package/routes/metadata.ts +71 -0
  192. package/routes/multimode.js +228 -263
  193. package/routes/multimode.ts +280 -0
  194. package/routes/nodes.js +378 -424
  195. package/routes/nodes.ts +455 -0
  196. package/routes/promptImport.js +291 -152
  197. package/routes/promptImport.ts +354 -0
  198. package/routes/prompts.js +333 -360
  199. package/routes/prompts.ts +379 -0
  200. package/routes/sessions.js +277 -285
  201. package/routes/sessions.ts +292 -0
  202. package/routes/storage.js +29 -31
  203. package/routes/storage.ts +39 -0
  204. package/server.js +189 -196
  205. package/server.ts +235 -0
  206. package/ui/dist/.vite/manifest.json +101 -0
  207. package/ui/dist/assets/CardNewsWorkspace-BJOCey7Z.js +2 -0
  208. package/ui/dist/assets/NodeCanvas-BZV40eAE.css +1 -0
  209. package/ui/dist/assets/NodeCanvas-C3dzYNsk.js +7 -0
  210. package/ui/dist/assets/PromptImportDialog-Dqu1VpUh.js +2 -0
  211. package/ui/dist/assets/PromptImportDiscoverySection-Dg8T9X0L.js +1 -0
  212. package/ui/dist/assets/PromptImportFolderSection-DBaqsFO4.js +1 -0
  213. package/ui/dist/assets/PromptLibraryPanel-p5QqR97M.js +2 -0
  214. package/ui/dist/assets/SettingsWorkspace-B5bSAZ6u.js +1 -0
  215. package/ui/dist/assets/index-C9cXwiWE.js +25 -0
  216. package/ui/dist/assets/index-CGMIkZXn.css +1 -0
  217. package/ui/dist/assets/index-Cvld7dUZ.js +1 -0
  218. package/ui/dist/index.html +6 -3
  219. package/assets/screenshot.png +0 -0
  220. package/assets/screenshots/classic-generate-light.png +0 -0
  221. package/assets/screenshots/node-graph-branching.png +0 -0
  222. package/assets/screenshots/settings-oauth-generation.png +0 -0
  223. package/assets/screenshots/settings-workspace.png +0 -0
  224. package/assets/screenshots/style-sheet-editor.png +0 -0
  225. package/integrations/comfyui/ima2_gen_bridge/__pycache__/__init__.cpython-313.pyc +0 -0
  226. package/integrations/comfyui/ima2_gen_bridge/__pycache__/nodes.cpython-313.pyc +0 -0
  227. package/ui/dist/assets/index-DARPdT4Q.css +0 -1
  228. package/ui/dist/assets/index-ht80GMq4.js +0 -31
  229. package/ui/dist/assets/index-ht80GMq4.js.map +0 -1
@@ -0,0 +1,309 @@
1
+ import { mkdir, writeFile } from "fs/promises";
2
+ import { join } from "path";
3
+ import { randomBytes } from "crypto";
4
+ import { summarizeReferencePayload, validateAndNormalizeRefs } from "../lib/refs.js";
5
+ import { classifyUpstreamError } from "../lib/errorClassify.js";
6
+ import { normalizeOAuthParams } from "../lib/oauthNormalize.js";
7
+ import { resolveProviderOptions } from "../lib/providerOptions.js";
8
+ import { generateViaResponses } from "../lib/responsesImageAdapter.js";
9
+ import { isNonRetryableGenerationError, normalizeGenerationFailure } from "../lib/generationErrors.js";
10
+ import { startJob, finishJob } from "../lib/inflight.js";
11
+ import { logEvent, logError } from "../lib/logger.js";
12
+ import { embedImageMetadataBestEffort } from "../lib/imageMetadataStore.js";
13
+
14
+ function validateModeration(ctx, moderation) {
15
+ if (typeof moderation !== "string" || !ctx.config.oauth.validModeration.has(moderation)) {
16
+ return { error: "moderation must be one of: auto, low" };
17
+ }
18
+ return { moderation };
19
+ }
20
+
21
+ export function registerGenerateRoutes(app, ctx) {
22
+ app.post("/api/generate", async (req, res) => {
23
+ const requestId = typeof req.body?.requestId === "string" ? req.body.requestId : req.id;
24
+ let finishStatus = "completed";
25
+ let finishHttpStatus;
26
+ let finishErrorCode;
27
+ let finishMeta = {};
28
+ try {
29
+ const sessionId = typeof req.body?.sessionId === "string" ? req.body.sessionId : null;
30
+ const clientNodeId = typeof req.body?.clientNodeId === "string" ? req.body.clientNodeId : null;
31
+ const {
32
+ prompt,
33
+ quality: rawQuality = "medium",
34
+ size = "1024x1024",
35
+ format = "png",
36
+ moderation = "low",
37
+ provider = "auto",
38
+ n = 1,
39
+ references = [],
40
+ mode: promptMode = "auto",
41
+ model: rawModel,
42
+ reasoningEffort: rawReasoningEffort,
43
+ webSearchEnabled: rawWebSearchEnabled = true,
44
+ } = req.body;
45
+ const { quality, warnings: qualityWarnings } = normalizeOAuthParams({ provider, quality: rawQuality });
46
+ const providerOptions = resolveProviderOptions(ctx, {
47
+ provider,
48
+ rawModel,
49
+ rawReasoningEffort,
50
+ rawSize: size,
51
+ rawWebSearchEnabled,
52
+ });
53
+ if (providerOptions.error) {
54
+ finishStatus = "error";
55
+ finishHttpStatus = providerOptions.status;
56
+ finishErrorCode = providerOptions.code;
57
+ return res.status(providerOptions.status).json({ error: providerOptions.error, code: providerOptions.code });
58
+ }
59
+ const imageModel = providerOptions.model;
60
+ const reasoningEffort = providerOptions.reasoningEffort;
61
+ const effectiveSize = providerOptions.size;
62
+ const webSearchEnabled = providerOptions.webSearchEnabled;
63
+ const activeProvider = providerOptions.provider;
64
+ const normalizedPromptMode = promptMode === "direct" ? "direct" : "auto";
65
+
66
+ if (!prompt) return res.status(400).json({ error: "Prompt is required" });
67
+ const moderationCheck = validateModeration(ctx, moderation);
68
+ if (moderationCheck.error) return res.status(400).json({ error: moderationCheck.error });
69
+ const count = Math.min(Math.max(parseInt(n) || 1, 1), 8);
70
+ const referencePayload = summarizeReferencePayload(references);
71
+
72
+ startJob({
73
+ requestId,
74
+ kind: "classic",
75
+ prompt,
76
+ meta: {
77
+ kind: "classic",
78
+ sessionId,
79
+ parentNodeId: null,
80
+ clientNodeId,
81
+ quality,
82
+ model: imageModel,
83
+ size: effectiveSize,
84
+ n: count,
85
+ refsCount: referencePayload.refsCount,
86
+ referenceBytes: referencePayload.referenceBytes,
87
+ referenceB64Chars: referencePayload.referenceB64Chars,
88
+ },
89
+ });
90
+
91
+ const refCheck = validateAndNormalizeRefs(references);
92
+ if (refCheck.error) {
93
+ finishStatus = "error";
94
+ finishHttpStatus = 400;
95
+ finishErrorCode = refCheck.code;
96
+ return res.status(400).json({ error: refCheck.error, code: refCheck.code });
97
+ }
98
+
99
+ const client = req.get("x-ima2-client") || "ui";
100
+ const referenceDiagnostics = refCheck.referenceDiagnostics || [];
101
+ const referenceMismatchCount = referenceDiagnostics.filter((ref) => ref.warnings?.includes("mime_mismatch")).length;
102
+ logEvent("generate", "request", {
103
+ requestId,
104
+ client,
105
+ provider: activeProvider,
106
+ quality,
107
+ model: imageModel,
108
+ size: effectiveSize,
109
+ moderation,
110
+ n: count,
111
+ refs: refCheck.refs.length,
112
+ referenceBytes: referencePayload.referenceBytes,
113
+ referenceMismatchCount,
114
+ refDetectedMimes: [...new Set(referenceDiagnostics.map((ref) => ref.detectedMime).filter(Boolean))].join(","),
115
+ refDeclaredMimes: [...new Set(referenceDiagnostics.map((ref) => ref.declaredMime).filter(Boolean))].join(","),
116
+ sessionId,
117
+ clientNodeId,
118
+ promptChars: typeof prompt === "string" ? prompt.length : 0,
119
+ promptMode: normalizedPromptMode,
120
+ webSearchEnabled,
121
+ });
122
+ const startTime = Date.now();
123
+
124
+ const mimeMap = { png: "image/png", jpeg: "image/jpeg", webp: "image/webp" };
125
+ const mime = mimeMap[format] || "image/png";
126
+ await mkdir(ctx.config.storage.generatedDir, { recursive: true });
127
+
128
+ const generateOne = async () => {
129
+ const MAX_RETRIES = 1;
130
+ let lastErr;
131
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
132
+ try {
133
+ const r = await generateViaResponses(
134
+ activeProvider,
135
+ prompt,
136
+ quality,
137
+ effectiveSize,
138
+ moderation,
139
+ refCheck.refDetails || refCheck.refs,
140
+ requestId,
141
+ normalizedPromptMode,
142
+ ctx,
143
+ { model: imageModel, reasoningEffort, webSearchEnabled },
144
+ );
145
+ if (r.b64) return r;
146
+ lastErr = new Error("Empty response (safety refusal)");
147
+ } catch (e) {
148
+ lastErr = e;
149
+ if (isNonRetryableGenerationError(e)) break;
150
+ }
151
+ if (attempt < MAX_RETRIES) {
152
+ logEvent("generate", "retry", { requestId, attempt: attempt + 1, errorCode: lastErr?.code });
153
+ }
154
+ }
155
+ throw normalizeGenerationFailure(lastErr, {
156
+ safetyMessage: "Content generation refused after retries",
157
+ });
158
+ };
159
+
160
+ const results = await Promise.allSettled(Array.from({ length: count }, generateOne));
161
+ const images = [];
162
+ let totalUsage = null;
163
+ let totalWebSearchCalls = 0;
164
+ for (const r of results) {
165
+ if (r.status === "fulfilled" && r.value.b64) {
166
+ const rand = randomBytes(ctx.config.ids.generatedHexBytes).toString("hex");
167
+ const filename = `${Date.now()}_${rand}_${images.length}.${format}`;
168
+ const meta = {
169
+ kind: "classic",
170
+ requestId,
171
+ sessionId,
172
+ clientNodeId,
173
+ prompt,
174
+ userPrompt: prompt,
175
+ revisedPrompt: r.value.revisedPrompt || null,
176
+ promptMode: normalizedPromptMode,
177
+ quality,
178
+ size: effectiveSize,
179
+ format,
180
+ moderation,
181
+ model: imageModel,
182
+ provider: activeProvider,
183
+ createdAt: Date.now(),
184
+ usage: r.value.usage || null,
185
+ webSearchCalls: r.value.webSearchCalls || 0,
186
+ webSearchEnabled,
187
+ refsCount: refCheck.refs.length,
188
+ };
189
+ const rawBuffer = Buffer.from(r.value.b64, "base64");
190
+ const embedded: any = await embedImageMetadataBestEffort(rawBuffer, format, meta, {
191
+ version: ctx.packageVersion,
192
+ });
193
+ if (!embedded.embedded) {
194
+ logEvent("generate", "metadata_embed_skipped", {
195
+ requestId,
196
+ filename,
197
+ code: embedded.code,
198
+ warning: embedded.warning,
199
+ });
200
+ }
201
+ await writeFile(join(ctx.config.storage.generatedDir, filename), embedded.buffer);
202
+ await writeFile(join(ctx.config.storage.generatedDir, filename + ".json"), JSON.stringify(meta)).catch(() => {});
203
+ images.push({
204
+ image: `data:${mime};base64,${r.value.b64}`,
205
+ filename,
206
+ revisedPrompt: r.value.revisedPrompt || null,
207
+ });
208
+ if (r.value.usage) {
209
+ if (!totalUsage) totalUsage = { ...r.value.usage };
210
+ else Object.keys(r.value.usage).forEach((k) => {
211
+ if (typeof r.value.usage[k] === "number") totalUsage[k] = (totalUsage[k] || 0) + r.value.usage[k];
212
+ });
213
+ }
214
+ if (typeof r.value.webSearchCalls === "number") totalWebSearchCalls += r.value.webSearchCalls;
215
+ } else if (r.status === "rejected") {
216
+ logError("generate", "parallel_failed", r.reason, { requestId });
217
+ }
218
+ }
219
+
220
+ if (images.length === 0) {
221
+ const firstErr = results.find((r) => r.status === "rejected")?.reason;
222
+ if (firstErr?.code) {
223
+ const status = firstErr.status || 500;
224
+ finishStatus = "error";
225
+ finishHttpStatus = status;
226
+ finishErrorCode = firstErr.code;
227
+ return res.status(status).json({
228
+ error: firstErr.message,
229
+ code: firstErr.code,
230
+ upstreamCode: firstErr.upstreamCode || null,
231
+ upstreamType: firstErr.upstreamType || null,
232
+ upstreamParam: firstErr.upstreamParam || null,
233
+ diagnosticReason: firstErr.diagnosticReason || null,
234
+ retryKind: firstErr.retryKind || null,
235
+ referencesDroppedOnRetry: firstErr.referencesDroppedOnRetry ?? null,
236
+ errorEventCount: firstErr.eventCount ?? null,
237
+ requestId,
238
+ });
239
+ }
240
+ finishStatus = "error";
241
+ finishHttpStatus = 500;
242
+ finishErrorCode = "GENERATE_ALL_FAILED";
243
+ return res.status(500).json({ error: "All generation attempts failed" });
244
+ }
245
+
246
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
247
+ const firstRevised = images[0]?.revisedPrompt || null;
248
+ const extra = {
249
+ usage: totalUsage,
250
+ provider: activeProvider,
251
+ webSearchCalls: totalWebSearchCalls,
252
+ quality,
253
+ size: effectiveSize,
254
+ moderation,
255
+ model: imageModel,
256
+ warnings: qualityWarnings,
257
+ revisedPrompt: firstRevised,
258
+ promptMode: normalizedPromptMode,
259
+ webSearchEnabled,
260
+ };
261
+
262
+ if (count === 1) {
263
+ finishHttpStatus = 200;
264
+ finishMeta = { filenames: [images[0].filename], imageCount: 1 };
265
+ logEvent("generate", "saved", {
266
+ requestId,
267
+ imageCount: 1,
268
+ elapsedMs: Date.now() - startTime,
269
+ filename: images[0].filename,
270
+ });
271
+ res.json({ image: images[0].image, elapsed, filename: images[0].filename, requestId, ...extra });
272
+ } else {
273
+ finishHttpStatus = 200;
274
+ finishMeta = { filenames: images.map((image) => image.filename), imageCount: images.length };
275
+ logEvent("generate", "saved", {
276
+ requestId,
277
+ imageCount: images.length,
278
+ elapsedMs: Date.now() - startTime,
279
+ });
280
+ res.json({ images, elapsed, count: images.length, requestId, ...extra });
281
+ }
282
+ } catch (err) {
283
+ const fallbackCode = err.code || classifyUpstreamError(err.message);
284
+ finishStatus = "error";
285
+ finishHttpStatus = err.status || 500;
286
+ finishErrorCode = fallbackCode || "GENERATE_FAILED";
287
+ logError("generate", "error", err, { requestId, code: finishErrorCode });
288
+ res.status(err.status || 500).json({
289
+ error: err.message,
290
+ code: fallbackCode,
291
+ upstreamCode: err.upstreamCode || null,
292
+ upstreamType: err.upstreamType || null,
293
+ upstreamParam: err.upstreamParam || null,
294
+ diagnosticReason: err.diagnosticReason || null,
295
+ retryKind: err.retryKind || null,
296
+ referencesDroppedOnRetry: err.referencesDroppedOnRetry ?? null,
297
+ errorEventCount: err.eventCount ?? null,
298
+ requestId,
299
+ });
300
+ } finally {
301
+ finishJob(requestId, {
302
+ status: finishStatus,
303
+ httpStatus: finishHttpStatus,
304
+ errorCode: finishErrorCode,
305
+ meta: finishMeta,
306
+ });
307
+ }
308
+ });
309
+ }
package/routes/health.js CHANGED
@@ -1,113 +1,108 @@
1
1
  import { listJobs, listTerminalJobs, finishJob } from "../lib/inflight.js";
2
-
3
2
  export function registerHealthRoutes(app, ctx) {
4
- const runtimePorts = () => ({
5
- backend: {
6
- configuredPort: Number(ctx.serverConfiguredPort || ctx.config.server.port),
7
- actualPort: Number(ctx.serverActualPort || ctx.config.server.port),
8
- url: ctx.serverUrl || `http://localhost:${ctx.serverActualPort || ctx.config.server.port}`,
9
- },
10
- oauth: {
11
- configuredPort: Number(ctx.oauthPort),
12
- actualPort: Number(ctx.oauthActualPort || ctx.oauthPort),
13
- url: ctx.oauthUrl,
14
- status: ctx.oauthReadyState,
15
- },
16
- });
17
-
18
- app.get("/api/providers", (_req, res) => {
19
- res.json({
20
- apiKey: false,
21
- oauth: true,
22
- oauthPort: ctx.oauthPort,
23
- oauthActualPort: ctx.oauthActualPort || ctx.oauthPort,
24
- oauthUrl: ctx.oauthUrl,
25
- apiKeyDisabled: true,
26
- runtime: runtimePorts(),
3
+ const runtimePorts = () => ({
4
+ backend: {
5
+ configuredPort: Number(ctx.serverConfiguredPort || ctx.config.server.port),
6
+ actualPort: Number(ctx.serverActualPort || ctx.config.server.port),
7
+ url: ctx.serverUrl || `http://localhost:${ctx.serverActualPort || ctx.config.server.port}`,
8
+ },
9
+ oauth: {
10
+ configuredPort: Number(ctx.oauthPort),
11
+ actualPort: Number(ctx.oauthActualPort || ctx.oauthPort),
12
+ url: ctx.oauthUrl,
13
+ status: ctx.oauthReadyState,
14
+ },
27
15
  });
28
- });
29
-
30
- app.get("/api/health", (_req, res) => {
31
- res.json({
32
- ok: true,
33
- version: ctx.packageVersion,
34
- provider: "oauth",
35
- uptimeSec: Math.round(process.uptime()),
36
- activeJobs: listJobs().length,
37
- pid: process.pid,
38
- startedAt: ctx.startedAt,
39
- runtime: runtimePorts(),
16
+ app.get("/api/providers", (_req, res) => {
17
+ res.json({
18
+ apiKey: Boolean(ctx.hasApiKey),
19
+ oauth: true,
20
+ oauthPort: ctx.oauthPort,
21
+ oauthActualPort: ctx.oauthActualPort || ctx.oauthPort,
22
+ oauthUrl: ctx.oauthUrl,
23
+ apiKeyDisabled: false,
24
+ apiKeySource: ctx.apiKeySource ?? "none",
25
+ runtime: runtimePorts(),
26
+ });
40
27
  });
41
- });
42
-
43
- app.get("/api/oauth/status", async (_req, res) => {
44
- if (ctx.oauthReadyState === "starting") {
45
- return res.json({ status: "starting", runtime: runtimePorts() });
46
- }
47
- if (ctx.oauthReadyState === "failed") {
48
- return res.json({ status: "offline", runtime: runtimePorts() });
49
- }
50
- try {
51
- const r = await fetch(`${ctx.oauthUrl}/v1/models`, {
52
- signal: AbortSignal.timeout(ctx.config.oauth.statusTimeoutMs),
53
- });
54
- if (r.ok) {
55
- const data = await r.json();
56
- res.json({ status: "ready", models: data.data?.map((m) => m.id) || [], runtime: runtimePorts() });
57
- } else {
58
- res.json({ status: "auth_required", runtime: runtimePorts() });
59
- }
60
- } catch {
61
- res.json({ status: "offline", runtime: runtimePorts() });
62
- }
63
- });
64
-
65
- app.get("/api/inflight", (req, res) => {
66
- const kind =
67
- typeof req.query.kind === "string" && req.query.kind.length > 0
68
- ? req.query.kind
69
- : undefined;
70
- const sessionId =
71
- typeof req.query.sessionId === "string" && req.query.sessionId.length > 0
72
- ? req.query.sessionId
73
- : undefined;
74
- const includeTerminal =
75
- req.query.includeTerminal === "1" || req.query.includeTerminal === "true";
76
- const jobs = listJobs({ kind, sessionId });
77
- if (!includeTerminal) return res.json({ jobs });
78
- return res.json({
79
- jobs,
80
- terminalJobs: listTerminalJobs({ kind, sessionId }),
28
+ app.get("/api/health", (_req, res) => {
29
+ res.json({
30
+ ok: true,
31
+ version: ctx.packageVersion,
32
+ provider: "oauth",
33
+ uptimeSec: Math.round(process.uptime()),
34
+ activeJobs: listJobs().length,
35
+ pid: process.pid,
36
+ startedAt: ctx.startedAt,
37
+ runtime: runtimePorts(),
38
+ });
39
+ });
40
+ app.get("/api/oauth/status", async (_req, res) => {
41
+ if (ctx.oauthReadyState === "starting") {
42
+ return res.json({ status: "starting", runtime: runtimePorts() });
43
+ }
44
+ if (ctx.oauthReadyState === "failed") {
45
+ return res.json({ status: "offline", runtime: runtimePorts() });
46
+ }
47
+ try {
48
+ const r = await fetch(`${ctx.oauthUrl}/v1/models`, {
49
+ signal: AbortSignal.timeout(ctx.config.oauth.statusTimeoutMs),
50
+ });
51
+ if (r.ok) {
52
+ const data = await r.json();
53
+ res.json({ status: "ready", models: data.data?.map((m) => m.id) || [], runtime: runtimePorts() });
54
+ }
55
+ else {
56
+ res.json({ status: "auth_required", runtime: runtimePorts() });
57
+ }
58
+ }
59
+ catch {
60
+ res.json({ status: "offline", runtime: runtimePorts() });
61
+ }
62
+ });
63
+ app.get("/api/inflight", (req, res) => {
64
+ const kind = typeof req.query.kind === "string" && req.query.kind.length > 0
65
+ ? req.query.kind
66
+ : undefined;
67
+ const sessionId = typeof req.query.sessionId === "string" && req.query.sessionId.length > 0
68
+ ? req.query.sessionId
69
+ : undefined;
70
+ const includeTerminal = req.query.includeTerminal === "1" || req.query.includeTerminal === "true";
71
+ const jobs = listJobs({ kind, sessionId });
72
+ if (!includeTerminal)
73
+ return res.json({ jobs });
74
+ return res.json({
75
+ jobs,
76
+ terminalJobs: listTerminalJobs({ kind, sessionId }),
77
+ });
78
+ });
79
+ app.delete("/api/inflight/:requestId", (req, res) => {
80
+ finishJob(req.params.requestId, { canceled: true });
81
+ res.status(204).end();
82
+ });
83
+ app.get("/api/billing", async (_req, res) => {
84
+ if (!ctx.hasApiKey) {
85
+ return res.json({ oauth: true, apiKeyValid: false, apiKeySource: "none" });
86
+ }
87
+ try {
88
+ const headers = { Authorization: `Bearer ${ctx.apiKey}`, "Content-Type": "application/json" };
89
+ const start = Math.floor(new Date(new Date().getFullYear(), new Date().getMonth(), 1).getTime() / 1000);
90
+ const end = Math.floor(Date.now() / 1000);
91
+ const [subRes, usageRes, modelsRes] = await Promise.allSettled([
92
+ fetch(`https://api.openai.com/v1/organization/costs?start_time=${start}&end_time=${end}&bucket_width=1d&limit=31`, { headers }),
93
+ fetch("https://api.openai.com/dashboard/billing/credit_grants", { headers }),
94
+ fetch("https://api.openai.com/v1/models", { headers }),
95
+ ]);
96
+ const billing = { apiKeySource: ctx.apiKeySource ?? "env" };
97
+ if (subRes.status === "fulfilled" && subRes.value.ok)
98
+ billing.costs = await subRes.value.json();
99
+ if (usageRes.status === "fulfilled" && usageRes.value.ok)
100
+ billing.credits = await usageRes.value.json();
101
+ billing.apiKeyValid = modelsRes.status === "fulfilled" && modelsRes.value.ok === true;
102
+ res.json(billing);
103
+ }
104
+ catch (err) {
105
+ res.status(500).json({ error: err.message, apiKeyValid: false });
106
+ }
81
107
  });
82
- });
83
-
84
- app.delete("/api/inflight/:requestId", (req, res) => {
85
- finishJob(req.params.requestId, { canceled: true });
86
- res.status(204).end();
87
- });
88
-
89
- app.get("/api/billing", async (_req, res) => {
90
- if (!ctx.hasApiKey) {
91
- return res.json({ oauth: true, apiKeyValid: false, apiKeySource: "none" });
92
- }
93
-
94
- try {
95
- const headers = { Authorization: `Bearer ${ctx.apiKey}`, "Content-Type": "application/json" };
96
- const start = Math.floor(new Date(new Date().getFullYear(), new Date().getMonth(), 1).getTime() / 1000);
97
- const end = Math.floor(Date.now() / 1000);
98
- const [subRes, usageRes, modelsRes] = await Promise.allSettled([
99
- fetch(`https://api.openai.com/v1/organization/costs?start_time=${start}&end_time=${end}&bucket_width=1d&limit=31`, { headers }),
100
- fetch("https://api.openai.com/dashboard/billing/credit_grants", { headers }),
101
- fetch("https://api.openai.com/v1/models", { headers }),
102
- ]);
103
-
104
- const billing = { apiKeySource: ctx.apiKeySource ?? "env" };
105
- if (subRes.status === "fulfilled" && subRes.value.ok) billing.costs = await subRes.value.json();
106
- if (usageRes.status === "fulfilled" && usageRes.value.ok) billing.credits = await usageRes.value.json();
107
- billing.apiKeyValid = modelsRes.status === "fulfilled" && modelsRes.value.ok === true;
108
- res.json(billing);
109
- } catch (err) {
110
- res.status(500).json({ error: err.message, apiKeyValid: false });
111
- }
112
- });
113
108
  }
@@ -0,0 +1,114 @@
1
+ import { listJobs, listTerminalJobs, finishJob } from "../lib/inflight.js";
2
+
3
+ export function registerHealthRoutes(app, ctx) {
4
+ const runtimePorts = () => ({
5
+ backend: {
6
+ configuredPort: Number(ctx.serverConfiguredPort || ctx.config.server.port),
7
+ actualPort: Number(ctx.serverActualPort || ctx.config.server.port),
8
+ url: ctx.serverUrl || `http://localhost:${ctx.serverActualPort || ctx.config.server.port}`,
9
+ },
10
+ oauth: {
11
+ configuredPort: Number(ctx.oauthPort),
12
+ actualPort: Number(ctx.oauthActualPort || ctx.oauthPort),
13
+ url: ctx.oauthUrl,
14
+ status: ctx.oauthReadyState,
15
+ },
16
+ });
17
+
18
+ app.get("/api/providers", (_req, res) => {
19
+ res.json({
20
+ apiKey: Boolean(ctx.hasApiKey),
21
+ oauth: true,
22
+ oauthPort: ctx.oauthPort,
23
+ oauthActualPort: ctx.oauthActualPort || ctx.oauthPort,
24
+ oauthUrl: ctx.oauthUrl,
25
+ apiKeyDisabled: false,
26
+ apiKeySource: ctx.apiKeySource ?? "none",
27
+ runtime: runtimePorts(),
28
+ });
29
+ });
30
+
31
+ app.get("/api/health", (_req, res) => {
32
+ res.json({
33
+ ok: true,
34
+ version: ctx.packageVersion,
35
+ provider: "oauth",
36
+ uptimeSec: Math.round(process.uptime()),
37
+ activeJobs: listJobs().length,
38
+ pid: process.pid,
39
+ startedAt: ctx.startedAt,
40
+ runtime: runtimePorts(),
41
+ });
42
+ });
43
+
44
+ app.get("/api/oauth/status", async (_req, res) => {
45
+ if (ctx.oauthReadyState === "starting") {
46
+ return res.json({ status: "starting", runtime: runtimePorts() });
47
+ }
48
+ if (ctx.oauthReadyState === "failed") {
49
+ return res.json({ status: "offline", runtime: runtimePorts() });
50
+ }
51
+ try {
52
+ const r = await fetch(`${ctx.oauthUrl}/v1/models`, {
53
+ signal: AbortSignal.timeout(ctx.config.oauth.statusTimeoutMs),
54
+ });
55
+ if (r.ok) {
56
+ const data: any = await r.json();
57
+ res.json({ status: "ready", models: data.data?.map((m) => m.id) || [], runtime: runtimePorts() });
58
+ } else {
59
+ res.json({ status: "auth_required", runtime: runtimePorts() });
60
+ }
61
+ } catch {
62
+ res.json({ status: "offline", runtime: runtimePorts() });
63
+ }
64
+ });
65
+
66
+ app.get("/api/inflight", (req, res) => {
67
+ const kind =
68
+ typeof req.query.kind === "string" && req.query.kind.length > 0
69
+ ? req.query.kind
70
+ : undefined;
71
+ const sessionId =
72
+ typeof req.query.sessionId === "string" && req.query.sessionId.length > 0
73
+ ? req.query.sessionId
74
+ : undefined;
75
+ const includeTerminal =
76
+ req.query.includeTerminal === "1" || req.query.includeTerminal === "true";
77
+ const jobs = listJobs({ kind, sessionId });
78
+ if (!includeTerminal) return res.json({ jobs });
79
+ return res.json({
80
+ jobs,
81
+ terminalJobs: listTerminalJobs({ kind, sessionId }),
82
+ });
83
+ });
84
+
85
+ app.delete("/api/inflight/:requestId", (req, res) => {
86
+ finishJob(req.params.requestId, { canceled: true });
87
+ res.status(204).end();
88
+ });
89
+
90
+ app.get("/api/billing", async (_req, res) => {
91
+ if (!ctx.hasApiKey) {
92
+ return res.json({ oauth: true, apiKeyValid: false, apiKeySource: "none" });
93
+ }
94
+
95
+ try {
96
+ const headers = { Authorization: `Bearer ${ctx.apiKey}`, "Content-Type": "application/json" };
97
+ const start = Math.floor(new Date(new Date().getFullYear(), new Date().getMonth(), 1).getTime() / 1000);
98
+ const end = Math.floor(Date.now() / 1000);
99
+ const [subRes, usageRes, modelsRes] = await Promise.allSettled([
100
+ fetch(`https://api.openai.com/v1/organization/costs?start_time=${start}&end_time=${end}&bucket_width=1d&limit=31`, { headers }),
101
+ fetch("https://api.openai.com/dashboard/billing/credit_grants", { headers }),
102
+ fetch("https://api.openai.com/v1/models", { headers }),
103
+ ]);
104
+
105
+ const billing: any = { apiKeySource: ctx.apiKeySource ?? "env" };
106
+ if (subRes.status === "fulfilled" && subRes.value.ok) billing.costs = await subRes.value.json();
107
+ if (usageRes.status === "fulfilled" && usageRes.value.ok) billing.credits = await usageRes.value.json();
108
+ billing.apiKeyValid = modelsRes.status === "fulfilled" && modelsRes.value.ok === true;
109
+ res.json(billing);
110
+ } catch (err) {
111
+ res.status(500).json({ error: err.message, apiKeyValid: false });
112
+ }
113
+ });
114
+ }