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,352 @@
1
+ import { setJobPhase } from "./inflight.js";
2
+ import { logEvent } from "./logger.js";
3
+ import { classifyUpstreamError, classifyUpstreamErrorCode } from "./errorClassify.js";
4
+ import { compressReferenceB64ForOAuth } from "./referenceImageCompress.js";
5
+ import { detectImageMimeFromB64 } from "./refs.js";
6
+ import {
7
+ AUTO_PROMPT_FIDELITY_SUFFIX,
8
+ DIRECT_PROMPT_FIDELITY_SUFFIX,
9
+ EDIT_DEVELOPER_PROMPT,
10
+ EDIT_NO_SEARCH_DEVELOPER_PROMPT,
11
+ GENERATE_DEVELOPER_PROMPT,
12
+ GENERATE_NO_SEARCH_DEVELOPER_PROMPT,
13
+ buildEditTextPrompt,
14
+ buildMultimodeSequencePrompt,
15
+ buildUserTextPrompt,
16
+ waitForOAuthReady,
17
+ } from "./oauthProxy.js";
18
+
19
+ function makeError(message, { status = 500, code = "RESPONSES_IMAGE_ERROR", cause, ...rest }: any = {}) {
20
+ const err: any = new Error(message);
21
+ err.status = status;
22
+ err.code = code;
23
+ if (cause) err.cause = cause;
24
+ Object.assign(err, rest);
25
+ return err;
26
+ }
27
+
28
+ function parseOpenAIErrorBody(text) {
29
+ try {
30
+ const parsed = JSON.parse(text);
31
+ const error = parsed?.error || {};
32
+ return {
33
+ message: typeof error.message === "string" && error.message ? error.message : "OpenAI request failed",
34
+ code: typeof error.code === "string" ? error.code : null,
35
+ type: typeof error.type === "string" ? error.type : null,
36
+ param: typeof error.param === "string" ? error.param : null,
37
+ };
38
+ } catch {
39
+ return null;
40
+ }
41
+ }
42
+
43
+ function normalizedCode(upstream) {
44
+ const byCode = classifyUpstreamErrorCode(upstream?.code);
45
+ if (byCode !== "UNKNOWN") return byCode;
46
+ const byType = classifyUpstreamErrorCode(upstream?.type);
47
+ if (byType !== "UNKNOWN") return byType;
48
+ const byMessage = classifyUpstreamError(upstream?.message);
49
+ return byMessage !== "UNKNOWN" ? byMessage : "RESPONSES_IMAGE_ERROR";
50
+ }
51
+
52
+ function safeUpstreamClientMessage(upstream, status) {
53
+ const code = normalizedCode(upstream);
54
+ if (code === "AUTH_API_KEY_INVALID") return "API key is invalid or unavailable.";
55
+ if (code === "MODERATION_REFUSED") return "OpenAI refused the image request for safety reasons.";
56
+ if (code === "INVALID_REQUEST") return "OpenAI rejected the image request parameters.";
57
+ if (status === 401 || status === 403) return "OpenAI authentication failed.";
58
+ if (status === 429) return "OpenAI rate limited the image request.";
59
+ return "OpenAI rejected the image request.";
60
+ }
61
+
62
+ async function getEndpoint(ctx, provider, scope) {
63
+ if (provider === "api") {
64
+ if (!ctx?.apiKey) {
65
+ throw makeError("API key is required for API provider image generation", {
66
+ status: 401,
67
+ code: "API_KEY_REQUIRED",
68
+ });
69
+ }
70
+ return {
71
+ url: "https://api.openai.com/v1/responses",
72
+ headers: {
73
+ "Content-Type": "application/json",
74
+ Accept: "text/event-stream",
75
+ Authorization: `Bearer ${ctx.apiKey}`,
76
+ },
77
+ };
78
+ }
79
+ await waitForOAuthReady(ctx);
80
+ const port = ctx?.config?.oauth?.proxyPort || 10531;
81
+ return {
82
+ url: `${ctx?.oauthUrl || `http://127.0.0.1:${port}`}/v1/responses`,
83
+ headers: { "Content-Type": "application/json", Accept: "text/event-stream" },
84
+ };
85
+ }
86
+
87
+ function tools(webSearchEnabled, imageOptions) {
88
+ return [
89
+ ...(webSearchEnabled ? [{ type: "web_search" }] : []),
90
+ { type: "image_generation", ...imageOptions },
91
+ ];
92
+ }
93
+
94
+ function normalizeRef(ref) {
95
+ const b64 = typeof ref === "string" ? ref : ref?.b64;
96
+ const detectedMime = typeof ref === "object" && ref?.detectedMime
97
+ ? ref.detectedMime
98
+ : detectImageMimeFromB64(b64);
99
+ const declaredMime = typeof ref === "object" ? ref?.declaredMime : null;
100
+ const mime = ["image/png", "image/jpeg", "image/webp"].includes(detectedMime)
101
+ ? detectedMime
102
+ : ["image/png", "image/jpeg", "image/webp"].includes(declaredMime)
103
+ ? declaredMime
104
+ : "image/png";
105
+ return { type: "input_image", image_url: `data:${mime};base64,${b64}` };
106
+ }
107
+
108
+ function extractSseData(block) {
109
+ let eventData = "";
110
+ for (const line of block.split("\n")) {
111
+ if (line.startsWith("data: ")) eventData += line.slice(6);
112
+ }
113
+ return eventData;
114
+ }
115
+
116
+ function extractPartialImage(data) {
117
+ if (typeof data?.type !== "string" || !data.type.includes("partial")) return null;
118
+ const item = data.item || {};
119
+ const b64 = data.partial_image || data.image || data.result || item.partial_image || item.image || item.result;
120
+ if (typeof b64 !== "string" || b64.length === 0) return null;
121
+ const index = Number.isFinite(data.index) ? data.index : Number.isFinite(item.index) ? item.index : null;
122
+ return { b64, index };
123
+ }
124
+
125
+ async function parseStream(res, { requestId, scope, maxImages = 1, onPartialImage = null }: any = {}) {
126
+ const reader = res.body.getReader();
127
+ const decoder = new TextDecoder();
128
+ const images = [];
129
+ const eventTypes = {};
130
+ let buffer = "";
131
+ let usage = null;
132
+ let webSearchCalls = 0;
133
+ let eventCount = 0;
134
+ let extraIgnored = 0;
135
+ while (true) {
136
+ const { done, value } = await reader.read();
137
+ if (done) break;
138
+ buffer += decoder.decode(value, { stream: true });
139
+ let boundary;
140
+ while ((boundary = buffer.indexOf("\n\n")) !== -1) {
141
+ const block = buffer.slice(0, boundary);
142
+ buffer = buffer.slice(boundary + 2);
143
+ const eventData = extractSseData(block);
144
+ if (!eventData || eventData === "[DONE]") continue;
145
+ let data;
146
+ try { data = JSON.parse(eventData); } catch { continue; }
147
+ eventCount++;
148
+ eventTypes[data.type || "_unknown"] = (eventTypes[data.type || "_unknown"] || 0) + 1;
149
+ const partial = extractPartialImage(data);
150
+ if (partial && typeof onPartialImage === "function") onPartialImage(partial);
151
+ if (data.type === "response.output_item.done" && data.item?.type === "image_generation_call") {
152
+ if (data.item.result && images.length < maxImages) {
153
+ images.push({
154
+ b64: data.item.result,
155
+ revisedPrompt: typeof data.item.revised_prompt === "string" ? data.item.revised_prompt : null,
156
+ });
157
+ if (requestId) setJobPhase(requestId, "decoding");
158
+ } else if (data.item.result) extraIgnored++;
159
+ }
160
+ if (data.type === "response.output_item.done" && data.item?.type === "web_search_call") webSearchCalls++;
161
+ if (data.type === "response.completed") {
162
+ usage = data.response?.usage || null;
163
+ const wsNum = data.response?.tool_usage?.web_search?.num_requests;
164
+ if (typeof wsNum === "number" && wsNum > webSearchCalls) webSearchCalls = wsNum;
165
+ }
166
+ if (data.type === "error") {
167
+ throw makeError("Responses stream returned an error", {
168
+ code: data.error?.code || "RESPONSES_STREAM_ERROR",
169
+ eventCount,
170
+ eventType: data.type,
171
+ });
172
+ }
173
+ }
174
+ }
175
+ logEvent(scope, "stream_end", { requestId, events: eventCount, imageCount: images.length });
176
+ return { images, usage, webSearchCalls, eventCount, eventTypes, extraIgnored };
177
+ }
178
+
179
+ async function parseJson(res, maxImages) {
180
+ const json: any = await res.json();
181
+ const images = [];
182
+ let webSearchCalls = 0;
183
+ for (const item of json.output || []) {
184
+ if (item.type === "image_generation_call" && item.result && images.length < maxImages) {
185
+ images.push({
186
+ b64: item.result,
187
+ revisedPrompt: typeof item.revised_prompt === "string" ? item.revised_prompt : null,
188
+ });
189
+ }
190
+ if (item.type === "web_search_call") webSearchCalls++;
191
+ }
192
+ return { images, usage: json.usage || null, webSearchCalls, eventCount: 0, eventTypes: {}, extraIgnored: 0 };
193
+ }
194
+
195
+ async function postResponses({ ctx, provider, scope, payload, requestId, maxImages = 1, onPartialImage = null }) {
196
+ const { url, headers } = await getEndpoint(ctx, provider, scope);
197
+ const timeoutMs = ctx?.config?.oauth?.generationTimeoutMs || 400 * 1000;
198
+ const controller = new AbortController();
199
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
200
+ try {
201
+ const res = await fetch(url, {
202
+ method: "POST",
203
+ headers,
204
+ signal: controller.signal,
205
+ body: JSON.stringify(payload),
206
+ });
207
+ logEvent(scope, "response", { requestId, provider, status: res.status, contentType: res.headers.get("content-type") });
208
+ if (!res.ok) {
209
+ const text = await res.text();
210
+ const upstream = parseOpenAIErrorBody(text);
211
+ if (res.status >= 400 && res.status < 500 && upstream?.message) {
212
+ throw makeError(safeUpstreamClientMessage(upstream, res.status), {
213
+ status: res.status,
214
+ code: normalizedCode(upstream),
215
+ upstreamBodyChars: text.length,
216
+ upstreamCode: upstream.code,
217
+ upstreamType: upstream.type,
218
+ upstreamParam: upstream.param,
219
+ upstreamMessageRedacted: true,
220
+ });
221
+ }
222
+ throw makeError(`${provider === "api" ? "OpenAI API" : "OAuth proxy"} returned ${res.status}`, {
223
+ status: res.status,
224
+ upstreamBodyChars: text.length,
225
+ });
226
+ }
227
+ if (requestId) setJobPhase(requestId, "streaming");
228
+ const contentType = res.headers.get("content-type") || "";
229
+ return contentType.includes("text/event-stream")
230
+ ? await parseStream(res, { requestId, scope, maxImages, onPartialImage })
231
+ : await parseJson(res, maxImages);
232
+ } catch (err) {
233
+ if (err?.name === "AbortError") {
234
+ throw makeError("Responses image generation timed out", { status: 504, code: "RESPONSES_IMAGE_TIMEOUT", cause: err });
235
+ }
236
+ throw err;
237
+ } finally {
238
+ clearTimeout(timer);
239
+ }
240
+ }
241
+
242
+ export async function generateViaResponses(provider, prompt, quality, size, moderation = "low", references = [], requestId = null, mode = "auto", ctx: any = {}, options: any = {}) {
243
+ const webSearchEnabled = options.webSearchEnabled !== false && options.searchMode !== "off";
244
+ const referenceInputs = references.map(normalizeRef);
245
+ const userContent = referenceInputs.length
246
+ ? [...referenceInputs, { type: "input_text", text: buildUserTextPrompt(prompt, mode, { webSearchEnabled }) }]
247
+ : buildUserTextPrompt(prompt, mode, { webSearchEnabled });
248
+ const result = await postResponses({
249
+ ctx,
250
+ provider,
251
+ scope: provider === "api" ? "api-generate" : "oauth",
252
+ requestId,
253
+ maxImages: 1,
254
+ onPartialImage: options.onPartialImage,
255
+ payload: {
256
+ model: options.model || ctx.config?.imageModels?.default || "gpt-5.4-mini",
257
+ input: [
258
+ { role: "developer", content: webSearchEnabled ? GENERATE_DEVELOPER_PROMPT : GENERATE_NO_SEARCH_DEVELOPER_PROMPT },
259
+ { role: "user", content: userContent },
260
+ ],
261
+ tools: tools(webSearchEnabled, { quality, size, moderation, ...(options.partialImages ? { partial_images: options.partialImages } : {}) }),
262
+ tool_choice: "required",
263
+ reasoning: { effort: options.reasoningEffort || "low" },
264
+ stream: true,
265
+ },
266
+ });
267
+ const image = result.images[0];
268
+ if (!image?.b64) throw makeError("No image data received from Responses API", { code: "EMPTY_RESPONSE", eventCount: result.eventCount });
269
+ return { b64: image.b64, usage: result.usage, webSearchCalls: result.webSearchCalls, revisedPrompt: image.revisedPrompt };
270
+ }
271
+
272
+ export async function generateMultimodeViaResponses(provider, prompt, quality, size, moderation = "low", references = [], requestId = null, mode = "auto", ctx: any = {}, options: any = {}) {
273
+ const maxImages = Math.min(8, Math.max(1, Math.trunc(Number(options.maxImages) || 1)));
274
+ const webSearchEnabled = options.webSearchEnabled !== false && options.searchMode !== "off";
275
+ const userText = buildMultimodeSequencePrompt(
276
+ mode === "direct"
277
+ ? `${prompt}${DIRECT_PROMPT_FIDELITY_SUFFIX}`
278
+ : `${prompt}${webSearchEnabled ? "" : ""}${AUTO_PROMPT_FIDELITY_SUFFIX}`,
279
+ maxImages,
280
+ { webSearchEnabled },
281
+ );
282
+ const referenceInputs = references.map(normalizeRef);
283
+ const userContent = referenceInputs.length
284
+ ? [...referenceInputs, { type: "input_text", text: userText }]
285
+ : userText;
286
+ return await postResponses({
287
+ ctx,
288
+ provider,
289
+ scope: provider === "api" ? "api-multimode" : "oauth-multimode",
290
+ requestId,
291
+ maxImages,
292
+ onPartialImage: options.onPartialImage,
293
+ payload: {
294
+ model: options.model || ctx.config?.imageModels?.default || "gpt-5.4-mini",
295
+ input: [
296
+ { role: "developer", content: `Create up to ${maxImages} separate image_generation_call outputs. Do not create a collage, grid, contact sheet, storyboard sheet, or multi-panel single image.` },
297
+ { role: "user", content: userContent },
298
+ ],
299
+ tools: tools(webSearchEnabled, { quality, size, moderation, ...(options.partialImages ? { partial_images: options.partialImages } : {}) }),
300
+ tool_choice: "required",
301
+ reasoning: { effort: options.reasoningEffort || "low" },
302
+ stream: true,
303
+ },
304
+ });
305
+ }
306
+
307
+ export async function editViaResponses(provider, prompt, imageB64, quality, size, moderation = "low", mode = "auto", ctx: any = {}, requestId = null, options: any = {}) {
308
+ const webSearchEnabled = options.webSearchEnabled !== false && options.searchMode !== "off";
309
+ const imageForRequest = await compressReferenceB64ForOAuth(imageB64, {
310
+ maxB64Bytes: ctx.config?.limits?.maxRefB64Bytes,
311
+ force: true,
312
+ });
313
+ const referenceImages = await Promise.all((Array.isArray(options.references) ? options.references : []).map((ref) =>
314
+ compressReferenceB64ForOAuth(typeof ref === "string" ? ref : ref?.b64, {
315
+ maxB64Bytes: ctx.config?.limits?.maxRefB64Bytes,
316
+ force: true,
317
+ }),
318
+ ));
319
+ const maskContent = typeof options.mask === "string" && options.mask.length > 0
320
+ ? [
321
+ { type: "input_image", image_url: `data:image/png;base64,${options.mask}` },
322
+ { type: "input_text", text: "The previous image is an edit mask guide. Use it as prompt guidance for where the edit should apply; it is not a visible final image element." },
323
+ ]
324
+ : [];
325
+ const userContent = [
326
+ { type: "input_image", image_url: `data:image/jpeg;base64,${imageForRequest.b64}` },
327
+ ...referenceImages.map(({ b64 }) => ({ type: "input_image", image_url: `data:image/jpeg;base64,${b64}` })),
328
+ ...maskContent,
329
+ { type: "input_text", text: buildEditTextPrompt(prompt, mode, { webSearchEnabled }) },
330
+ ];
331
+ const result = await postResponses({
332
+ ctx,
333
+ provider,
334
+ scope: provider === "api" ? "api-edit" : "oauth-edit",
335
+ requestId,
336
+ maxImages: 1,
337
+ payload: {
338
+ model: options.model || ctx.config?.imageModels?.default || "gpt-5.4-mini",
339
+ input: [
340
+ { role: "developer", content: webSearchEnabled ? EDIT_DEVELOPER_PROMPT : EDIT_NO_SEARCH_DEVELOPER_PROMPT },
341
+ { role: "user", content: userContent },
342
+ ],
343
+ tools: tools(webSearchEnabled, { quality, size, moderation }),
344
+ tool_choice: "required",
345
+ reasoning: { effort: options.reasoningEffort || "low" },
346
+ stream: true,
347
+ },
348
+ });
349
+ const image = result.images[0];
350
+ if (!image?.b64) throw makeError("No image data received from Responses edit", { code: "EMPTY_RESPONSE", eventCount: result.eventCount });
351
+ return { b64: image.b64, usage: result.usage, revisedPrompt: image.revisedPrompt, webSearchCalls: result.webSearchCalls };
352
+ }
@@ -1,93 +1,91 @@
1
1
  import { createServer } from "node:net";
2
-
3
2
  const DEFAULT_MAX_ATTEMPTS = 20;
4
-
5
3
  export function parseLocalhostPortFromUrl(url) {
6
- try {
7
- const parsed = new URL(url);
8
- const port = Number(parsed.port);
9
- return Number.isFinite(port) && port > 0 ? port : null;
10
- } catch {
11
- return null;
12
- }
4
+ try {
5
+ const parsed = new URL(url);
6
+ const port = Number(parsed.port);
7
+ return Number.isFinite(port) && port > 0 ? port : null;
8
+ }
9
+ catch {
10
+ return null;
11
+ }
13
12
  }
14
-
15
13
  export function stripV1FromOAuthUrl(url) {
16
- return String(url || "").replace(/\/v1\/?$/, "");
14
+ return String(url || "").replace(/\/v1\/?$/, "");
17
15
  }
18
-
19
16
  export function parseOAuthReadyUrl(line) {
20
- const text = String(line || "");
21
- const match = text.match(/https?:\/\/(?:127\.0\.0\.1|localhost):\d+(?:\/v1)?/i);
22
- return match ? stripV1FromOAuthUrl(match[0]) : null;
17
+ const text = String(line || "");
18
+ const match = text.match(/https?:\/\/(?:127\.0\.0\.1|localhost):\d+(?:\/v1)?/i);
19
+ return match ? stripV1FromOAuthUrl(match[0]) : null;
23
20
  }
24
-
25
21
  function checkPort(port, host) {
26
- return new Promise((resolve, reject) => {
27
- const probe = createServer()
28
- .once("error", (err) => {
29
- probe.close(() => {});
30
- reject(err);
31
- })
32
- .once("listening", () => {
33
- probe.close(() => resolve(true));
34
- });
35
- if (host) probe.listen(port, host);
36
- else probe.listen(port);
37
- });
22
+ return new Promise((resolve, reject) => {
23
+ const probe = createServer()
24
+ .once("error", (err) => {
25
+ probe.close(() => { });
26
+ reject(err);
27
+ })
28
+ .once("listening", () => {
29
+ probe.close(() => resolve(true));
30
+ });
31
+ if (host)
32
+ probe.listen(port, host);
33
+ else
34
+ probe.listen(port);
35
+ });
38
36
  }
39
-
40
37
  export async function findAvailablePort(startPort, options = {}) {
41
- const maxAttempts = options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
42
- const host = options.host;
43
- for (let offset = 0; offset <= maxAttempts; offset++) {
44
- const port = Number(startPort) + offset;
45
- try {
46
- await checkPort(port, host);
47
- return port;
48
- } catch (err) {
49
- if (err?.code !== "EADDRINUSE") throw err;
38
+ const maxAttempts = options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
39
+ const host = options.host;
40
+ for (let offset = 0; offset <= maxAttempts; offset++) {
41
+ const port = Number(startPort) + offset;
42
+ try {
43
+ await checkPort(port, host);
44
+ return port;
45
+ }
46
+ catch (err) {
47
+ if (err?.code !== "EADDRINUSE")
48
+ throw err;
49
+ }
50
50
  }
51
- }
52
- const err = new Error(`No available port found from ${startPort} to ${Number(startPort) + maxAttempts}`);
53
- err.code = "PORT_RANGE_EXHAUSTED";
54
- throw err;
51
+ const err = new Error(`No available port found from ${startPort} to ${Number(startPort) + maxAttempts}`);
52
+ err.code = "PORT_RANGE_EXHAUSTED";
53
+ throw err;
55
54
  }
56
-
57
55
  function listenOnce(app, port, host) {
58
- return new Promise((resolve, reject) => {
59
- const server = host ? app.listen(port, host) : app.listen(port);
60
- server.once("listening", () => resolve(server));
61
- server.once("error", (err) => reject(err));
62
- });
56
+ return new Promise((resolve, reject) => {
57
+ const server = host ? app.listen(port, host) : app.listen(port);
58
+ server.once("listening", () => resolve(server));
59
+ server.once("error", (err) => reject(err));
60
+ });
63
61
  }
64
-
65
62
  export async function listenWithPortFallback(app, startPort, options = {}) {
66
- const maxAttempts = options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
67
- const host = options.host;
68
- const label = options.label || "server";
69
- for (let offset = 0; offset <= maxAttempts; offset++) {
70
- const port = Number(startPort) + offset;
71
- try {
72
- const server = await listenOnce(app, port, host);
73
- if (offset > 0 && typeof options.onFallback === "function") {
74
- options.onFallback({ label, requestedPort: Number(startPort), actualPort: port });
75
- }
76
- return server;
77
- } catch (err) {
78
- if (err?.code !== "EADDRINUSE") throw err;
79
- if (offset >= maxAttempts) {
80
- const exhausted = new Error(`${label} port range exhausted from ${startPort} to ${port}`);
81
- exhausted.code = "PORT_RANGE_EXHAUSTED";
82
- exhausted.cause = err;
83
- throw exhausted;
84
- }
63
+ const maxAttempts = options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
64
+ const host = options.host;
65
+ const label = options.label || "server";
66
+ for (let offset = 0; offset <= maxAttempts; offset++) {
67
+ const port = Number(startPort) + offset;
68
+ try {
69
+ const server = await listenOnce(app, port, host);
70
+ if (offset > 0 && typeof options.onFallback === "function") {
71
+ options.onFallback({ label, requestedPort: Number(startPort), actualPort: port });
72
+ }
73
+ return server;
74
+ }
75
+ catch (err) {
76
+ if (err?.code !== "EADDRINUSE")
77
+ throw err;
78
+ if (offset >= maxAttempts) {
79
+ const exhausted = new Error(`${label} port range exhausted from ${startPort} to ${port}`);
80
+ exhausted.code = "PORT_RANGE_EXHAUSTED";
81
+ exhausted.cause = err;
82
+ throw exhausted;
83
+ }
84
+ }
85
85
  }
86
- }
87
- throw new Error(`${label} failed to bind`);
86
+ throw new Error(`${label} failed to bind`);
88
87
  }
89
-
90
88
  export function getServerPort(server) {
91
- const address = server?.address?.();
92
- return typeof address === "object" && address ? address.port : null;
89
+ const address = server?.address?.();
90
+ return typeof address === "object" && address ? address.port : null;
93
91
  }
@@ -0,0 +1,93 @@
1
+ import { createServer } from "node:net";
2
+
3
+ const DEFAULT_MAX_ATTEMPTS = 20;
4
+
5
+ export function parseLocalhostPortFromUrl(url) {
6
+ try {
7
+ const parsed = new URL(url);
8
+ const port = Number(parsed.port);
9
+ return Number.isFinite(port) && port > 0 ? port : null;
10
+ } catch {
11
+ return null;
12
+ }
13
+ }
14
+
15
+ export function stripV1FromOAuthUrl(url) {
16
+ return String(url || "").replace(/\/v1\/?$/, "");
17
+ }
18
+
19
+ export function parseOAuthReadyUrl(line) {
20
+ const text = String(line || "");
21
+ const match = text.match(/https?:\/\/(?:127\.0\.0\.1|localhost):\d+(?:\/v1)?/i);
22
+ return match ? stripV1FromOAuthUrl(match[0]) : null;
23
+ }
24
+
25
+ function checkPort(port, host) {
26
+ return new Promise((resolve, reject) => {
27
+ const probe = createServer()
28
+ .once("error", (err) => {
29
+ probe.close(() => {});
30
+ reject(err);
31
+ })
32
+ .once("listening", () => {
33
+ probe.close(() => resolve(true));
34
+ });
35
+ if (host) probe.listen(port, host);
36
+ else probe.listen(port);
37
+ });
38
+ }
39
+
40
+ export async function findAvailablePort(startPort, options: any = {}) {
41
+ const maxAttempts = options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
42
+ const host = options.host;
43
+ for (let offset = 0; offset <= maxAttempts; offset++) {
44
+ const port = Number(startPort) + offset;
45
+ try {
46
+ await checkPort(port, host);
47
+ return port;
48
+ } catch (err) {
49
+ if ((err as any)?.code !== "EADDRINUSE") throw err;
50
+ }
51
+ }
52
+ const err: any = new Error(`No available port found from ${startPort} to ${Number(startPort) + maxAttempts}`);
53
+ err.code = "PORT_RANGE_EXHAUSTED";
54
+ throw err;
55
+ }
56
+
57
+ function listenOnce(app, port, host) {
58
+ return new Promise((resolve, reject) => {
59
+ const server = host ? app.listen(port, host) : app.listen(port);
60
+ server.once("listening", () => resolve(server));
61
+ server.once("error", (err) => reject(err));
62
+ });
63
+ }
64
+
65
+ export async function listenWithPortFallback(app, startPort, options: any = {}) {
66
+ const maxAttempts = options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
67
+ const host = options.host;
68
+ const label = options.label || "server";
69
+ for (let offset = 0; offset <= maxAttempts; offset++) {
70
+ const port = Number(startPort) + offset;
71
+ try {
72
+ const server = await listenOnce(app, port, host);
73
+ if (offset > 0 && typeof options.onFallback === "function") {
74
+ options.onFallback({ label, requestedPort: Number(startPort), actualPort: port });
75
+ }
76
+ return server;
77
+ } catch (err) {
78
+ if ((err as any)?.code !== "EADDRINUSE") throw err;
79
+ if (offset >= maxAttempts) {
80
+ const exhausted: any = new Error(`${label} port range exhausted from ${startPort} to ${port}`);
81
+ exhausted.code = "PORT_RANGE_EXHAUSTED";
82
+ exhausted.cause = err;
83
+ throw exhausted;
84
+ }
85
+ }
86
+ }
87
+ throw new Error(`${label} failed to bind`);
88
+ }
89
+
90
+ export function getServerPort(server) {
91
+ const address = server?.address?.();
92
+ return typeof address === "object" && address ? address.port : null;
93
+ }