ima2-gen 1.1.13 → 1.1.14

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 (53) hide show
  1. package/README.md +10 -1
  2. package/bin/commands/doctor.js +195 -0
  3. package/bin/commands/doctor.ts +202 -0
  4. package/bin/ima2.js +3 -105
  5. package/bin/ima2.ts +3 -109
  6. package/config.js +1 -0
  7. package/config.ts +5 -0
  8. package/docs/CLI.md +36 -0
  9. package/docs/FAQ.ko.md +82 -2
  10. package/docs/FAQ.md +85 -2
  11. package/docs/PROMPT_STUDIO.ko.md +111 -0
  12. package/docs/PROMPT_STUDIO.md +115 -0
  13. package/docs/README.ko.md +8 -1
  14. package/docs/migration/runtime-test-inventory.md +6 -1
  15. package/lib/agentRuntime.js +9 -2
  16. package/lib/agentRuntime.ts +8 -2
  17. package/lib/errorClassify.js +1 -1
  18. package/lib/errorClassify.ts +1 -1
  19. package/lib/generationErrors.js +121 -23
  20. package/lib/generationErrors.ts +100 -13
  21. package/lib/responsesDoctor.js +386 -0
  22. package/lib/responsesDoctor.ts +456 -0
  23. package/lib/responsesErrors.js +57 -0
  24. package/lib/responsesErrors.ts +83 -0
  25. package/lib/responsesFallback.js +72 -0
  26. package/lib/responsesFallback.ts +114 -0
  27. package/lib/responsesImageAdapter.js +121 -174
  28. package/lib/responsesImageAdapter.ts +136 -211
  29. package/lib/responsesParse.js +324 -0
  30. package/lib/responsesParse.ts +452 -0
  31. package/lib/responsesTools.js +15 -0
  32. package/lib/responsesTools.ts +28 -0
  33. package/package.json +1 -1
  34. package/routes/edit.js +26 -1
  35. package/routes/edit.ts +26 -1
  36. package/routes/generate.js +40 -0
  37. package/routes/generate.ts +47 -0
  38. package/ui/dist/.vite/manifest.json +12 -12
  39. package/ui/dist/assets/{AgentWorkspace-BJe9yxPA.js → AgentWorkspace-B6YNOZHi.js} +1 -1
  40. package/ui/dist/assets/{CardNewsWorkspace-BBLdwzYU.js → CardNewsWorkspace-EFVeg4l_.js} +1 -1
  41. package/ui/dist/assets/{NodeCanvas-BSZ527J4.js → NodeCanvas-iM6yjHvO.js} +1 -1
  42. package/ui/dist/assets/{PromptBuilderPanel-Y2VygFc0.js → PromptBuilderPanel-C3GdLDCl.js} +1 -1
  43. package/ui/dist/assets/{PromptImportDialog-C6lFV-LL.js → PromptImportDialog-DS9vrc_w.js} +2 -2
  44. package/ui/dist/assets/{PromptImportDiscoverySection-D8YJFhND.js → PromptImportDiscoverySection-DHFEt_FA.js} +1 -1
  45. package/ui/dist/assets/{PromptImportFolderSection-ywfcQolW.js → PromptImportFolderSection-BQxb1zs5.js} +1 -1
  46. package/ui/dist/assets/{PromptLibraryPanel-fk4KmrGy.js → PromptLibraryPanel-NhMKVGfU.js} +2 -2
  47. package/ui/dist/assets/{SettingsWorkspace-DL5vhAHQ.js → SettingsWorkspace-FjKjaDqj.js} +1 -1
  48. package/ui/dist/assets/index-BAN6lKgf.js +28 -0
  49. package/ui/dist/assets/{index-BLx55BOg.js → index-BbFZyM92.js} +1 -1
  50. package/ui/dist/assets/index-DK1faG9Z.css +1 -0
  51. package/ui/dist/index.html +2 -2
  52. package/ui/dist/assets/index-ByViUJfx.css +0 -1
  53. package/ui/dist/assets/index-Ci36vcFD.js +0 -28
@@ -0,0 +1,452 @@
1
+ import { setJobPhase } from "./inflight.js";
2
+ import { logEvent } from "./logger.js";
3
+
4
+ export interface ParsedImage {
5
+ b64: string;
6
+ revisedPrompt: string | null;
7
+ }
8
+
9
+ export type FinalImageHandler = (image: ParsedImage, index: number) => Promise<void> | void;
10
+
11
+ export interface ResponseOutputSummary {
12
+ eventType: string;
13
+ itemType: string | null;
14
+ status: string | null;
15
+ hasResult: boolean;
16
+ resultChars: number;
17
+ revisedPromptChars: number;
18
+ hasError: boolean;
19
+ errorCode: string | null;
20
+ errorType: string | null;
21
+ errorParam: string | null;
22
+ }
23
+
24
+ export interface ResponseDiagnostics {
25
+ eventTypes: Record<string, number>;
26
+ streamStats: {
27
+ chunkCount: number;
28
+ bytesRead: number;
29
+ maxChunkBytes: number;
30
+ lfBoundaryCount: number;
31
+ crlfBoundaryCount: number;
32
+ parseSkipCount: number;
33
+ finalBufferChars: number;
34
+ sawDoneSentinel: boolean;
35
+ sawResponseCompleted: boolean;
36
+ };
37
+ outputItemSummary: ResponseOutputSummary[];
38
+ imageCallSeen: boolean;
39
+ imageCallCompleted: boolean;
40
+ imageCallFailed: boolean;
41
+ imageResultCount: number;
42
+ webSearchCallSeen: boolean;
43
+ messageOutputSeen: boolean;
44
+ outputTextChars: number;
45
+ }
46
+
47
+ export interface ParsedResponsesResult {
48
+ images: ParsedImage[];
49
+ usage: Record<string, number> | null;
50
+ webSearchCalls: number;
51
+ eventCount: number;
52
+ eventTypes: Record<string, number>;
53
+ extraIgnored: number;
54
+ text: string | null;
55
+ diagnostics: ResponseDiagnostics;
56
+ }
57
+
58
+ interface SseItem {
59
+ type?: string;
60
+ partial_image_b64?: string;
61
+ partial_image_index?: number;
62
+ partial_image?: string;
63
+ image?: string;
64
+ result?: string;
65
+ index?: number;
66
+ revised_prompt?: string;
67
+ status?: string;
68
+ error?: {
69
+ code?: string;
70
+ type?: string;
71
+ param?: string;
72
+ };
73
+ content?: Array<{ type?: string; text?: string }>;
74
+ }
75
+
76
+ interface SseData {
77
+ type?: string;
78
+ delta?: string;
79
+ text?: string;
80
+ item?: SseItem;
81
+ partial_image_b64?: string;
82
+ partial_image_index?: number;
83
+ partial_image?: string;
84
+ image?: string;
85
+ result?: string;
86
+ index?: number;
87
+ response?: {
88
+ usage?: Record<string, number>;
89
+ output?: SseItem[];
90
+ tool_usage?: { web_search?: { num_requests?: number } };
91
+ };
92
+ error?: { code?: string };
93
+ }
94
+
95
+ interface ParseState {
96
+ images: ParsedImage[];
97
+ eventTypes: Record<string, number>;
98
+ outputItemSummary: ResponseOutputSummary[];
99
+ usage: Record<string, number> | null;
100
+ textOutput: string;
101
+ finalTextOutput: string | null;
102
+ webSearchCalls: number;
103
+ eventCount: number;
104
+ extraIgnored: number;
105
+ chunkCount: number;
106
+ bytesRead: number;
107
+ maxChunkBytes: number;
108
+ lfBoundaryCount: number;
109
+ crlfBoundaryCount: number;
110
+ parseSkipCount: number;
111
+ finalBufferChars: number;
112
+ sawDoneSentinel: boolean;
113
+ sawResponseCompleted: boolean;
114
+ imageCallSeen: boolean;
115
+ imageCallCompleted: boolean;
116
+ imageCallFailed: boolean;
117
+ imageResultCount: number;
118
+ webSearchCallSeen: boolean;
119
+ messageOutputSeen: boolean;
120
+ }
121
+
122
+ function createState(): ParseState {
123
+ return {
124
+ images: [],
125
+ eventTypes: {},
126
+ outputItemSummary: [],
127
+ usage: null,
128
+ textOutput: "",
129
+ finalTextOutput: null,
130
+ webSearchCalls: 0,
131
+ eventCount: 0,
132
+ extraIgnored: 0,
133
+ chunkCount: 0,
134
+ bytesRead: 0,
135
+ maxChunkBytes: 0,
136
+ lfBoundaryCount: 0,
137
+ crlfBoundaryCount: 0,
138
+ parseSkipCount: 0,
139
+ finalBufferChars: 0,
140
+ sawDoneSentinel: false,
141
+ sawResponseCompleted: false,
142
+ imageCallSeen: false,
143
+ imageCallCompleted: false,
144
+ imageCallFailed: false,
145
+ imageResultCount: 0,
146
+ webSearchCallSeen: false,
147
+ messageOutputSeen: false,
148
+ };
149
+ }
150
+
151
+ const MAX_DIAGNOSTIC_LABEL_CHARS = 120;
152
+ const UNSAFE_DIAGNOSTIC_LABEL = /(bearer\s+|sk-[a-z0-9_-]{4,}|data:image\/|https?:\/\/|[a-z][a-z0-9+.-]*:\/\/|@|[\r\n])/i;
153
+ const SAFE_DIAGNOSTIC_LABEL = /^[A-Za-z0-9_.:[\]-]+$/;
154
+
155
+ export function safeDiagnosticLabel(value: unknown, fallback: string | null = null): string | null {
156
+ if (typeof value !== "string" || value.length === 0) return fallback;
157
+ const trimmed = value.slice(0, MAX_DIAGNOSTIC_LABEL_CHARS);
158
+ if (UNSAFE_DIAGNOSTIC_LABEL.test(trimmed)) return "_redacted";
159
+ if (!SAFE_DIAGNOSTIC_LABEL.test(trimmed)) return "_redacted";
160
+ return trimmed;
161
+ }
162
+
163
+ function extractSseData(block: string): string {
164
+ let eventData = "";
165
+ for (const rawLine of block.split(/\r?\n/)) {
166
+ const line = rawLine.replace(/\r$/, "");
167
+ if (line.startsWith("data:")) eventData += line.slice(5).trimStart();
168
+ }
169
+ return eventData;
170
+ }
171
+
172
+ function nextSseBlock(buffer: string): { block: string; rest: string; delimiter: string } | null {
173
+ const match = /\r?\n\r?\n/.exec(buffer);
174
+ if (!match) return null;
175
+ return {
176
+ block: buffer.slice(0, match.index),
177
+ rest: buffer.slice(match.index + match[0].length),
178
+ delimiter: match[0],
179
+ };
180
+ }
181
+
182
+ function extractPartialImage(data: SseData): { b64: string; index: number | null } | null {
183
+ if (typeof data?.type !== "string" || !data.type.includes("partial")) return null;
184
+ const item = data.item || {};
185
+ const b64 =
186
+ data.partial_image_b64 ||
187
+ data.partial_image ||
188
+ data.image ||
189
+ data.result ||
190
+ item.partial_image_b64 ||
191
+ item.partial_image ||
192
+ item.image ||
193
+ item.result;
194
+ if (typeof b64 !== "string" || b64.length === 0) return null;
195
+ const index =
196
+ typeof data.partial_image_index === "number" && Number.isFinite(data.partial_image_index)
197
+ ? data.partial_image_index
198
+ : typeof data.index === "number" && Number.isFinite(data.index)
199
+ ? data.index
200
+ : typeof item.partial_image_index === "number" && Number.isFinite(item.partial_image_index)
201
+ ? item.partial_image_index
202
+ : typeof item.index === "number" && Number.isFinite(item.index)
203
+ ? item.index
204
+ : null;
205
+ return { b64, index };
206
+ }
207
+
208
+ function extractTextDelta(data: SseData): string | null {
209
+ if (data.type === "response.output_text.delta" && typeof data.delta === "string") return data.delta;
210
+ return null;
211
+ }
212
+
213
+ function extractFinalText(data: SseData): string | null {
214
+ if (data.type === "response.output_text.done" && typeof data.text === "string") return cleanTextOutput(data.text);
215
+ if (data.type === "response.output_item.done" && data.item?.type === "message") {
216
+ return extractJsonItemText(data.item);
217
+ }
218
+ return null;
219
+ }
220
+
221
+ function extractJsonItemText(item: { type?: string; text?: string; content?: Array<{ type?: string; text?: string }> }): string | null {
222
+ if (item.type === "output_text" && typeof item.text === "string") return cleanTextOutput(item.text);
223
+ if (!Array.isArray(item.content)) return null;
224
+ const text = item.content
225
+ .filter((part) => part.type === "output_text" && typeof part.text === "string")
226
+ .map((part) => part.text)
227
+ .join("\n\n");
228
+ return cleanTextOutput(text);
229
+ }
230
+
231
+ function cleanTextOutput(value: string): string | null {
232
+ const trimmed = value.trim();
233
+ return trimmed ? trimmed.slice(0, 4_000) : null;
234
+ }
235
+
236
+ function summarizeItem(eventType: string, item: SseItem): ResponseOutputSummary {
237
+ const result = typeof item.result === "string" ? item.result : "";
238
+ const revised = typeof item.revised_prompt === "string" ? item.revised_prompt : "";
239
+ return {
240
+ eventType: safeDiagnosticLabel(eventType, "_unknown") || "_unknown",
241
+ itemType: safeDiagnosticLabel(item.type),
242
+ status: safeDiagnosticLabel(item.status),
243
+ hasResult: result.length > 0,
244
+ resultChars: result.length,
245
+ revisedPromptChars: revised.length,
246
+ hasError: Boolean(item.error),
247
+ errorCode: safeDiagnosticLabel(item.error?.code),
248
+ errorType: safeDiagnosticLabel(item.error?.type),
249
+ errorParam: safeDiagnosticLabel(item.error?.param),
250
+ };
251
+ }
252
+
253
+ function recordOutputItem(state: ParseState, eventType: string, item: SseItem | undefined): void {
254
+ if (!item) return;
255
+ const summary = summarizeItem(eventType, item);
256
+ state.outputItemSummary.push(summary);
257
+ if (item.type === "image_generation_call") {
258
+ state.imageCallSeen = true;
259
+ state.imageCallCompleted = state.imageCallCompleted || eventType === "response.output_item.done" || item.status === "completed";
260
+ state.imageCallFailed = state.imageCallFailed || item.status === "failed" || Boolean(item.error);
261
+ if (summary.hasResult) state.imageResultCount++;
262
+ }
263
+ if (item.type === "web_search_call") state.webSearchCallSeen = true;
264
+ if (item.type === "message") state.messageOutputSeen = true;
265
+ }
266
+
267
+ function diagnosticsFromState(state: ParseState): ResponseDiagnostics {
268
+ const outputTextChars = (state.finalTextOutput ?? cleanTextOutput(state.textOutput) ?? "").length;
269
+ return {
270
+ eventTypes: state.eventTypes,
271
+ streamStats: {
272
+ chunkCount: state.chunkCount,
273
+ bytesRead: state.bytesRead,
274
+ maxChunkBytes: state.maxChunkBytes,
275
+ lfBoundaryCount: state.lfBoundaryCount,
276
+ crlfBoundaryCount: state.crlfBoundaryCount,
277
+ parseSkipCount: state.parseSkipCount,
278
+ finalBufferChars: state.finalBufferChars,
279
+ sawDoneSentinel: state.sawDoneSentinel,
280
+ sawResponseCompleted: state.sawResponseCompleted,
281
+ },
282
+ outputItemSummary: state.outputItemSummary,
283
+ imageCallSeen: state.imageCallSeen,
284
+ imageCallCompleted: state.imageCallCompleted,
285
+ imageCallFailed: state.imageCallFailed,
286
+ imageResultCount: state.imageResultCount,
287
+ webSearchCallSeen: state.webSearchCallSeen,
288
+ messageOutputSeen: state.messageOutputSeen,
289
+ outputTextChars,
290
+ };
291
+ }
292
+
293
+ function resultFromState(state: ParseState): ParsedResponsesResult {
294
+ const text = state.finalTextOutput ?? cleanTextOutput(state.textOutput);
295
+ return {
296
+ images: state.images,
297
+ usage: state.usage,
298
+ webSearchCalls: state.webSearchCalls,
299
+ eventCount: state.eventCount,
300
+ eventTypes: state.eventTypes,
301
+ extraIgnored: state.extraIgnored,
302
+ text,
303
+ diagnostics: diagnosticsFromState(state),
304
+ };
305
+ }
306
+
307
+ interface ParseStreamOptions {
308
+ requestId?: string | null;
309
+ scope: string;
310
+ maxImages?: number;
311
+ onPartialImage?: ((partial: { b64: string; index: number | null | undefined }) => void) | null;
312
+ onFinalImage?: FinalImageHandler | null;
313
+ }
314
+
315
+ function makeStreamError(message: string, code: string, eventCount: number, eventType: string): Error {
316
+ const err = new Error(message) as Error & { code?: string; upstreamCode?: string; status?: number; eventCount?: number; eventType?: string };
317
+ err.code = "RESPONSES_STREAM_ERROR";
318
+ err.upstreamCode = code;
319
+ err.status = 502;
320
+ err.eventCount = eventCount;
321
+ err.eventType = eventType;
322
+ Object.defineProperty(err, "ima2ResponsesError", { value: true });
323
+ return err;
324
+ }
325
+
326
+ async function appendFinalImageFromItem(
327
+ state: ParseState,
328
+ item: SseItem,
329
+ maxImages: number,
330
+ requestId: string | null | undefined,
331
+ onFinalImage: FinalImageHandler | null,
332
+ ): Promise<void> {
333
+ if (item.type !== "image_generation_call" || typeof item.result !== "string" || !item.result) return;
334
+ if (state.images.some((image) => image.b64 === item.result)) return;
335
+ if (state.images.length < maxImages) {
336
+ const image = {
337
+ b64: item.result,
338
+ revisedPrompt: typeof item.revised_prompt === "string" ? item.revised_prompt : null,
339
+ };
340
+ const index = state.images.length;
341
+ state.images.push(image);
342
+ if (requestId) setJobPhase(requestId, "decoding");
343
+ await onFinalImage?.(image, index);
344
+ } else {
345
+ state.extraIgnored++;
346
+ }
347
+ }
348
+
349
+ export async function parseStream(res: Response, {
350
+ requestId,
351
+ scope,
352
+ maxImages = 1,
353
+ onPartialImage = null,
354
+ onFinalImage = null,
355
+ }: ParseStreamOptions): Promise<ParsedResponsesResult> {
356
+ const reader = res.body!.getReader();
357
+ const decoder = new TextDecoder();
358
+ const state = createState();
359
+ let buffer = "";
360
+ while (true) {
361
+ const { done, value } = await reader.read();
362
+ if (done) break;
363
+ state.chunkCount++;
364
+ state.bytesRead += value.byteLength;
365
+ state.maxChunkBytes = Math.max(state.maxChunkBytes, value.byteLength);
366
+ buffer += decoder.decode(value, { stream: true });
367
+ let next;
368
+ while ((next = nextSseBlock(buffer)) !== null) {
369
+ const block = next.block;
370
+ buffer = next.rest;
371
+ if (next.delimiter.includes("\r\n")) state.crlfBoundaryCount++;
372
+ else state.lfBoundaryCount++;
373
+ const eventData = extractSseData(block);
374
+ if (eventData === "[DONE]") {
375
+ state.sawDoneSentinel = true;
376
+ continue;
377
+ }
378
+ if (!eventData) continue;
379
+ let data: SseData;
380
+ try { data = JSON.parse(eventData); } catch { state.parseSkipCount++; continue; }
381
+ state.eventCount++;
382
+ const eventType = safeDiagnosticLabel(data.type, "_unknown") || "_unknown";
383
+ state.eventTypes[eventType] = (state.eventTypes[eventType] || 0) + 1;
384
+ const delta = extractTextDelta(data);
385
+ if (delta) state.textOutput += delta;
386
+ const finalText = extractFinalText(data);
387
+ if (finalText) state.finalTextOutput = finalText;
388
+ if (finalText) state.messageOutputSeen = true;
389
+ const partial = extractPartialImage(data);
390
+ if (partial && typeof onPartialImage === "function") onPartialImage(partial);
391
+ if (data.type === "response.output_item.done") recordOutputItem(state, data.type, data.item);
392
+ if (data.type === "response.output_item.done" && data.item?.type === "image_generation_call") {
393
+ await appendFinalImageFromItem(state, data.item, maxImages, requestId, onFinalImage);
394
+ }
395
+ if (data.type === "response.output_item.done" && data.item?.type === "web_search_call") state.webSearchCalls++;
396
+ if (data.type === "response.completed") {
397
+ state.sawResponseCompleted = true;
398
+ state.usage = data.response?.usage || null;
399
+ for (const item of data.response?.output || []) {
400
+ recordOutputItem(state, data.type, item);
401
+ await appendFinalImageFromItem(state, item, maxImages, requestId, onFinalImage);
402
+ }
403
+ const wsNum = data.response?.tool_usage?.web_search?.num_requests;
404
+ if (typeof wsNum === "number" && wsNum > state.webSearchCalls) state.webSearchCalls = wsNum;
405
+ }
406
+ if (data.type === "error") {
407
+ throw makeStreamError(
408
+ "Responses stream returned an error",
409
+ safeDiagnosticLabel(data.error?.code, "RESPONSES_STREAM_ERROR") || "RESPONSES_STREAM_ERROR",
410
+ state.eventCount,
411
+ eventType,
412
+ );
413
+ }
414
+ }
415
+ }
416
+ state.finalBufferChars = buffer.length;
417
+ logEvent(scope, "stream_end", {
418
+ requestId,
419
+ events: state.eventCount,
420
+ imageCount: state.images.length,
421
+ webSearchCalls: state.webSearchCalls,
422
+ imageCallSeen: state.imageCallSeen,
423
+ messageOutputSeen: state.messageOutputSeen,
424
+ bytesRead: state.bytesRead,
425
+ parseSkipCount: state.parseSkipCount,
426
+ });
427
+ return resultFromState(state);
428
+ }
429
+
430
+ export async function parseJson(res: Response, maxImages: number): Promise<ParsedResponsesResult> {
431
+ const json = await res.json() as {
432
+ output?: SseItem[];
433
+ usage?: Record<string, number>;
434
+ };
435
+ const state = createState();
436
+ state.usage = json.usage || null;
437
+ for (const item of json.output || []) {
438
+ state.eventCount++;
439
+ state.eventTypes["json.output"] = (state.eventTypes["json.output"] || 0) + 1;
440
+ recordOutputItem(state, "json.output", item);
441
+ if (item.type === "image_generation_call" && item.result && state.images.length < maxImages) {
442
+ state.images.push({
443
+ b64: item.result,
444
+ revisedPrompt: typeof item.revised_prompt === "string" ? item.revised_prompt : null,
445
+ });
446
+ }
447
+ if (item.type === "web_search_call") state.webSearchCalls++;
448
+ const itemText = extractJsonItemText(item);
449
+ if (itemText) state.textOutput += `${state.textOutput ? "\n\n" : ""}${itemText}`;
450
+ }
451
+ return resultFromState(state);
452
+ }
@@ -0,0 +1,15 @@
1
+ export function tools(webSearchEnabled, imageOptions) {
2
+ return [
3
+ ...(webSearchEnabled ? [{ type: "web_search" }] : []),
4
+ { type: "image_generation", ...imageOptions },
5
+ ];
6
+ }
7
+ export function toolTypes(requestTools) {
8
+ return requestTools.map((tool) => tool.type);
9
+ }
10
+ export function imageToolChoice(forceImageTool) {
11
+ return forceImageTool ? { type: "image_generation" } : "required";
12
+ }
13
+ export function imageToolChoiceKind(choice) {
14
+ return choice === "required" ? "required" : "image_generation";
15
+ }
@@ -0,0 +1,28 @@
1
+ export interface ImageGenOptions {
2
+ quality?: string;
3
+ size?: string;
4
+ moderation?: string;
5
+ partial_images?: number;
6
+ }
7
+
8
+ export type ResponseTool = { type: string; quality?: string; size?: string; moderation?: string; partial_images?: number };
9
+ export type ImageToolChoice = "required" | { type: "image_generation" };
10
+
11
+ export function tools(webSearchEnabled: boolean, imageOptions: ImageGenOptions): ResponseTool[] {
12
+ return [
13
+ ...(webSearchEnabled ? [{ type: "web_search" }] : []),
14
+ { type: "image_generation", ...imageOptions },
15
+ ];
16
+ }
17
+
18
+ export function toolTypes(requestTools: ResponseTool[]): string[] {
19
+ return requestTools.map((tool) => tool.type);
20
+ }
21
+
22
+ export function imageToolChoice(forceImageTool: boolean): ImageToolChoice {
23
+ return forceImageTool ? { type: "image_generation" } : "required";
24
+ }
25
+
26
+ export function imageToolChoiceKind(choice: ImageToolChoice): "required" | "image_generation" {
27
+ return choice === "required" ? "required" : "image_generation";
28
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ima2-gen",
3
- "version": "1.1.13",
3
+ "version": "1.1.14",
4
4
  "description": "Local OAuth image generation studio with classic and node workflows",
5
5
  "type": "module",
6
6
  "bin": {
package/routes/edit.js CHANGED
@@ -203,6 +203,7 @@ export function registerEditRoutes(app, ctxRaw) {
203
203
  }
204
204
  catch (e) {
205
205
  const err = errInfo(e);
206
+ const ext = (err.raw && typeof err.raw === "object" ? err.raw : {});
206
207
  const fallbackCode = err.code || classifyUpstreamError(err.message);
207
208
  if (isGenerationCanceledError(err.raw) || isJobCanceled(requestId)) {
208
209
  const canceled = makeGenerationCanceledError();
@@ -219,7 +220,31 @@ export function registerEditRoutes(app, ctxRaw) {
219
220
  finishHttpStatus = err.status || 500;
220
221
  finishErrorCode = fallbackCode || "EDIT_FAILED";
221
222
  logError("edit", "error", err.raw, { requestId, code: finishErrorCode });
222
- res.status(err.status || 500).json({ error: err.message, code: fallbackCode });
223
+ res.status(err.status || 500).json({
224
+ error: err.message,
225
+ code: fallbackCode,
226
+ upstreamCode: ext.upstreamCode || null,
227
+ upstreamType: ext.upstreamType || null,
228
+ upstreamParam: ext.upstreamParam || null,
229
+ diagnosticReason: ext.diagnosticReason || null,
230
+ retryKind: ext.retryKind || null,
231
+ initialEventCount: ext.initialEventCount ?? null,
232
+ initialEventTypes: ext.initialEventTypes || null,
233
+ referencesDroppedOnRetry: ext.referencesDroppedOnRetry ?? null,
234
+ developerPromptDroppedOnRetry: ext.developerPromptDroppedOnRetry ?? null,
235
+ webSearchDroppedOnRetry: ext.webSearchDroppedOnRetry ?? null,
236
+ fallbackEventCount: ext.fallbackEventCount ?? null,
237
+ fallbackEventTypes: ext.fallbackEventTypes || null,
238
+ fallbackImageCallSeen: ext.fallbackImageCallSeen ?? null,
239
+ fallbackImageResultCount: ext.fallbackImageResultCount ?? null,
240
+ errorEventCount: ext.eventCount ?? null,
241
+ eventTypes: ext.eventTypes || null,
242
+ webSearchCalls: ext.webSearchCalls ?? null,
243
+ responseDiagnostics: ext.responseDiagnostics || null,
244
+ toolTypes: ext.toolTypes || null,
245
+ toolChoiceKind: ext.toolChoiceKind || null,
246
+ requestId,
247
+ });
223
248
  }
224
249
  finally {
225
250
  finishJob(requestId, {
package/routes/edit.ts CHANGED
@@ -251,6 +251,7 @@ export function registerEditRoutes(app: Express, ctxRaw: RouteRuntimeContext) {
251
251
  });
252
252
  } catch (e) {
253
253
  const err = errInfo(e);
254
+ const ext = (err.raw && typeof err.raw === "object" ? err.raw as Record<string, unknown> : {});
254
255
  const fallbackCode = err.code || classifyUpstreamError(err.message);
255
256
  if (isGenerationCanceledError(err.raw) || isJobCanceled(requestId)) {
256
257
  const canceled = makeGenerationCanceledError();
@@ -267,7 +268,31 @@ export function registerEditRoutes(app: Express, ctxRaw: RouteRuntimeContext) {
267
268
  finishHttpStatus = err.status || 500;
268
269
  finishErrorCode = fallbackCode || "EDIT_FAILED";
269
270
  logError("edit", "error", err.raw, { requestId, code: finishErrorCode });
270
- res.status(err.status || 500).json({ error: err.message, code: fallbackCode });
271
+ res.status(err.status || 500).json({
272
+ error: err.message,
273
+ code: fallbackCode,
274
+ upstreamCode: ext.upstreamCode || null,
275
+ upstreamType: ext.upstreamType || null,
276
+ upstreamParam: ext.upstreamParam || null,
277
+ diagnosticReason: ext.diagnosticReason || null,
278
+ retryKind: ext.retryKind || null,
279
+ initialEventCount: ext.initialEventCount ?? null,
280
+ initialEventTypes: ext.initialEventTypes || null,
281
+ referencesDroppedOnRetry: ext.referencesDroppedOnRetry ?? null,
282
+ developerPromptDroppedOnRetry: ext.developerPromptDroppedOnRetry ?? null,
283
+ webSearchDroppedOnRetry: ext.webSearchDroppedOnRetry ?? null,
284
+ fallbackEventCount: ext.fallbackEventCount ?? null,
285
+ fallbackEventTypes: ext.fallbackEventTypes || null,
286
+ fallbackImageCallSeen: ext.fallbackImageCallSeen ?? null,
287
+ fallbackImageResultCount: ext.fallbackImageResultCount ?? null,
288
+ errorEventCount: ext.eventCount ?? null,
289
+ eventTypes: ext.eventTypes || null,
290
+ webSearchCalls: ext.webSearchCalls ?? null,
291
+ responseDiagnostics: ext.responseDiagnostics || null,
292
+ toolTypes: ext.toolTypes || null,
293
+ toolChoiceKind: ext.toolChoiceKind || null,
294
+ requestId,
295
+ });
271
296
  } finally {
272
297
  finishJob(requestId, {
273
298
  canceled: finishCanceled,
@@ -130,6 +130,7 @@ export function registerGenerateRoutes(app, ctxRaw) {
130
130
  reasoningEffort,
131
131
  webSearchEnabled,
132
132
  signal: cancelController.signal,
133
+ allowPromptOnlyOAuthFallback: activeProvider !== "api",
133
134
  });
134
135
  throwIfJobCanceled(requestId);
135
136
  if (r.b64)
@@ -157,9 +158,21 @@ export function registerGenerateRoutes(app, ctxRaw) {
157
158
  const images = [];
158
159
  let totalUsage = null;
159
160
  let totalWebSearchCalls = 0;
161
+ let firstRetryMeta = null;
160
162
  for (const r of results) {
161
163
  if (r.status === "fulfilled" && r.value.b64) {
162
164
  throwIfJobCanceled(requestId);
165
+ const retryValue = r.value;
166
+ if (!firstRetryMeta && retryValue.retryKind) {
167
+ firstRetryMeta = {
168
+ retryKind: retryValue.retryKind,
169
+ initialEventCount: retryValue.initialEventCount ?? null,
170
+ initialEventTypes: retryValue.initialEventTypes || null,
171
+ referencesDroppedOnRetry: retryValue.referencesDroppedOnRetry ?? null,
172
+ developerPromptDroppedOnRetry: retryValue.developerPromptDroppedOnRetry ?? null,
173
+ webSearchDroppedOnRetry: retryValue.webSearchDroppedOnRetry ?? null,
174
+ };
175
+ }
163
176
  const rand = randomBytes(ctx.config.ids.generatedHexBytes).toString("hex");
164
177
  const filename = `${Date.now()}_${rand}_${images.length}.${format}`;
165
178
  const meta = {
@@ -249,8 +262,21 @@ export function registerGenerateRoutes(app, ctxRaw) {
249
262
  upstreamParam: firstErr.upstreamParam || null,
250
263
  diagnosticReason: firstErr.diagnosticReason || null,
251
264
  retryKind: firstErr.retryKind || null,
265
+ initialEventCount: firstErr.initialEventCount ?? null,
266
+ initialEventTypes: firstErr.initialEventTypes || null,
252
267
  referencesDroppedOnRetry: firstErr.referencesDroppedOnRetry ?? null,
268
+ developerPromptDroppedOnRetry: firstErr.developerPromptDroppedOnRetry ?? null,
269
+ webSearchDroppedOnRetry: firstErr.webSearchDroppedOnRetry ?? null,
270
+ fallbackEventCount: firstErr.fallbackEventCount ?? null,
271
+ fallbackEventTypes: firstErr.fallbackEventTypes || null,
272
+ fallbackImageCallSeen: firstErr.fallbackImageCallSeen ?? null,
273
+ fallbackImageResultCount: firstErr.fallbackImageResultCount ?? null,
253
274
  errorEventCount: firstErr.eventCount ?? null,
275
+ eventTypes: firstErr.eventTypes || null,
276
+ webSearchCalls: firstErr.webSearchCalls ?? null,
277
+ responseDiagnostics: firstErr.responseDiagnostics || null,
278
+ toolTypes: firstErr.toolTypes || null,
279
+ toolChoiceKind: firstErr.toolChoiceKind || null,
254
280
  requestId,
255
281
  });
256
282
  }
@@ -273,6 +299,7 @@ export function registerGenerateRoutes(app, ctxRaw) {
273
299
  revisedPrompt: firstRevised,
274
300
  promptMode: normalizedPromptMode,
275
301
  webSearchEnabled,
302
+ ...(firstRetryMeta || {}),
276
303
  };
277
304
  if (count === 1) {
278
305
  finishHttpStatus = 200;
@@ -323,8 +350,21 @@ export function registerGenerateRoutes(app, ctxRaw) {
323
350
  upstreamParam: ext.upstreamParam || null,
324
351
  diagnosticReason: ext.diagnosticReason || null,
325
352
  retryKind: ext.retryKind || null,
353
+ initialEventCount: ext.initialEventCount ?? null,
354
+ initialEventTypes: ext.initialEventTypes || null,
326
355
  referencesDroppedOnRetry: ext.referencesDroppedOnRetry ?? null,
356
+ developerPromptDroppedOnRetry: ext.developerPromptDroppedOnRetry ?? null,
357
+ webSearchDroppedOnRetry: ext.webSearchDroppedOnRetry ?? null,
358
+ fallbackEventCount: ext.fallbackEventCount ?? null,
359
+ fallbackEventTypes: ext.fallbackEventTypes || null,
360
+ fallbackImageCallSeen: ext.fallbackImageCallSeen ?? null,
361
+ fallbackImageResultCount: ext.fallbackImageResultCount ?? null,
327
362
  errorEventCount: ext.eventCount ?? null,
363
+ eventTypes: ext.eventTypes || null,
364
+ webSearchCalls: ext.webSearchCalls ?? null,
365
+ responseDiagnostics: ext.responseDiagnostics || null,
366
+ toolTypes: ext.toolTypes || null,
367
+ toolChoiceKind: ext.toolChoiceKind || null,
328
368
  requestId,
329
369
  });
330
370
  }