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
@@ -1,10 +1,24 @@
1
- import { setJobPhase } from "./inflight.js";
2
1
  import { logEvent } from "./logger.js";
3
2
  import { classifyUpstreamError, classifyUpstreamErrorCode } from "./errorClassify.js";
4
3
  import { compressReferenceB64ForOAuth } from "./referenceImageCompress.js";
5
4
  import { detectImageMimeFromB64 } from "./refs.js";
6
5
  import { errInfo } from "./errInfo.js";
6
+ import { setJobPhase } from "./inflight.js";
7
7
  import { type RouteRuntimeContext, requireRuntimeContext } from "./runtimeContext.js";
8
+ import {
9
+ parseJson,
10
+ parseStream,
11
+ safeDiagnosticLabel,
12
+ type FinalImageHandler,
13
+ } from "./responsesParse.js";
14
+ import {
15
+ imageToolChoice,
16
+ imageToolChoiceKind,
17
+ tools,
18
+ toolTypes,
19
+ } from "./responsesTools.js";
20
+ import { emptyResponseError } from "./responsesErrors.js";
21
+ import { retryPromptOnlyJsonImage } from "./responsesFallback.js";
8
22
  import {
9
23
  AUTO_PROMPT_FIDELITY_SUFFIX,
10
24
  DIRECT_PROMPT_FIDELITY_SUFFIX,
@@ -20,9 +34,6 @@ import {
20
34
  waitForOAuthReady,
21
35
  } from "./oauthProxy.js";
22
36
 
23
- interface ParsedImage { b64: string; revisedPrompt: string | null; }
24
- type FinalImageHandler = (image: ParsedImage, index: number) => Promise<void> | void;
25
-
26
37
  interface MakeErrorOptions {
27
38
  status?: number;
28
39
  code?: string;
@@ -37,12 +48,15 @@ interface ResponsesError extends Error {
37
48
  [key: string]: unknown;
38
49
  }
39
50
 
51
+ const RESPONSES_ERROR_MARKER = "ima2ResponsesError";
52
+
40
53
  function makeError(message: string, { status = 500, code = "RESPONSES_IMAGE_ERROR", cause, ...rest }: MakeErrorOptions = {}): ResponsesError {
41
54
  const err = new Error(message) as ResponsesError;
42
55
  err.status = status;
43
56
  err.code = code;
44
57
  if (cause) err.cause = cause;
45
58
  Object.assign(err, rest);
59
+ Object.defineProperty(err, RESPONSES_ERROR_MARKER, { value: true });
46
60
  return err;
47
61
  }
48
62
 
@@ -59,9 +73,9 @@ function parseOpenAIErrorBody(text: string): UpstreamError | null {
59
73
  const error = parsed?.error || {};
60
74
  return {
61
75
  message: typeof error.message === "string" && error.message ? error.message : "OpenAI request failed",
62
- code: typeof error.code === "string" ? error.code : null,
63
- type: typeof error.type === "string" ? error.type : null,
64
- param: typeof error.param === "string" ? error.param : null,
76
+ code: safeDiagnosticLabel(error.code),
77
+ type: safeDiagnosticLabel(error.type),
78
+ param: safeDiagnosticLabel(error.param),
65
79
  };
66
80
  } catch {
67
81
  return null;
@@ -87,45 +101,61 @@ function safeUpstreamClientMessage(upstream: UpstreamError | null | undefined, s
87
101
  return "OpenAI rejected the image request.";
88
102
  }
89
103
 
104
+ function safeBaseUrl(value: string) {
105
+ try {
106
+ const parsed = new URL(value);
107
+ parsed.username = "";
108
+ parsed.password = "";
109
+ return parsed.toString().replace(/\/$/, "");
110
+ } catch {
111
+ return value.replace(/\/$/, "");
112
+ }
113
+ }
114
+
115
+ function apiAuthorizationHeader(apiKey: string | undefined) {
116
+ const key = typeof apiKey === "string" ? apiKey.trim() : "";
117
+ if (!key) {
118
+ throw makeError("API key is required for API provider image generation", {
119
+ status: 401,
120
+ code: "API_KEY_REQUIRED",
121
+ });
122
+ }
123
+ if (/[\u0000-\u001f\u007f]/.test(key)) {
124
+ throw makeError("API key contains invalid characters.", {
125
+ status: 401,
126
+ code: "AUTH_API_KEY_INVALID",
127
+ });
128
+ }
129
+ return `Bearer ${key}`;
130
+ }
131
+
132
+ function isKnownResponsesError(value: unknown) {
133
+ return Boolean(
134
+ value &&
135
+ typeof value === "object" &&
136
+ (value as { ima2ResponsesError?: unknown }).ima2ResponsesError === true,
137
+ );
138
+ }
139
+
90
140
  async function getEndpoint(ctx: RouteRuntimeContext, provider: string | undefined, _scope: string) {
91
141
  if (provider === "api") {
92
- if (!ctx?.apiKey) {
93
- throw makeError("API key is required for API provider image generation", {
94
- status: 401,
95
- code: "API_KEY_REQUIRED",
96
- });
97
- }
98
142
  return {
99
143
  url: "https://api.openai.com/v1/responses",
100
144
  headers: {
101
145
  "Content-Type": "application/json",
102
146
  Accept: "text/event-stream",
103
- Authorization: `Bearer ${ctx.apiKey}`,
147
+ Authorization: apiAuthorizationHeader(ctx.apiKey),
104
148
  },
105
149
  };
106
150
  }
107
151
  await waitForOAuthReady(ctx);
108
152
  const port = ctx?.config?.oauth?.proxyPort || 10531;
109
153
  return {
110
- url: `${ctx?.oauthUrl || `http://127.0.0.1:${port}`}/v1/responses`,
154
+ url: `${safeBaseUrl(ctx?.oauthUrl || `http://127.0.0.1:${port}`)}/v1/responses`,
111
155
  headers: { "Content-Type": "application/json", Accept: "text/event-stream" },
112
156
  };
113
157
  }
114
158
 
115
- interface ImageGenOptions {
116
- quality?: string;
117
- size?: string;
118
- moderation?: string;
119
- partial_images?: number;
120
- }
121
-
122
- function tools(webSearchEnabled: boolean, imageOptions: ImageGenOptions) {
123
- return [
124
- ...(webSearchEnabled ? [{ type: "web_search" }] : []),
125
- { type: "image_generation", ...imageOptions },
126
- ];
127
- }
128
-
129
159
  type ReferenceRef = string | { b64?: string; detectedMime?: string | null; declaredMime?: string | null };
130
160
 
131
161
  function normalizeRef(ref: ReferenceRef) {
@@ -142,177 +172,6 @@ function normalizeRef(ref: ReferenceRef) {
142
172
  return { type: "input_image", image_url: `data:${mime};base64,${b64}` };
143
173
  }
144
174
 
145
- function extractSseData(block: string) {
146
- let eventData = "";
147
- for (const line of block.split("\n")) {
148
- if (line.startsWith("data: ")) eventData += line.slice(6);
149
- }
150
- return eventData;
151
- }
152
-
153
- interface SseData {
154
- type?: string;
155
- delta?: string;
156
- text?: string;
157
- item?: {
158
- type?: string;
159
- partial_image?: string;
160
- image?: string;
161
- result?: string;
162
- index?: number;
163
- revised_prompt?: string;
164
- content?: Array<{ type?: string; text?: string }>;
165
- };
166
- partial_image?: string;
167
- image?: string;
168
- result?: string;
169
- index?: number;
170
- response?: { usage?: Record<string, number>; tool_usage?: { web_search?: { num_requests?: number } } };
171
- error?: { code?: string };
172
- }
173
-
174
- function extractPartialImage(data: SseData) {
175
- if (typeof data?.type !== "string" || !data.type.includes("partial")) return null;
176
- const item = data.item || {};
177
- const b64 = data.partial_image || data.image || data.result || item.partial_image || item.image || item.result;
178
- if (typeof b64 !== "string" || b64.length === 0) return null;
179
- const index = Number.isFinite(data.index) ? data.index : Number.isFinite(item.index) ? item.index : null;
180
- return { b64, index };
181
- }
182
-
183
- function extractTextDelta(data: SseData): string | null {
184
- if (data.type === "response.output_text.delta" && typeof data.delta === "string") return data.delta;
185
- return null;
186
- }
187
-
188
- function extractFinalText(data: SseData): string | null {
189
- if (data.type === "response.output_text.done" && typeof data.text === "string") return cleanTextOutput(data.text);
190
- if (data.type === "response.output_item.done" && data.item?.type === "message") {
191
- return extractJsonItemText(data.item);
192
- }
193
- return null;
194
- }
195
-
196
- function extractJsonItemText(item: { type?: string; text?: string; content?: Array<{ type?: string; text?: string }> }): string | null {
197
- if (item.type === "output_text" && typeof item.text === "string") return cleanTextOutput(item.text);
198
- if (!Array.isArray(item.content)) return null;
199
- const text = item.content
200
- .filter((part) => part.type === "output_text" && typeof part.text === "string")
201
- .map((part) => part.text)
202
- .join("\n\n");
203
- return cleanTextOutput(text);
204
- }
205
-
206
- function cleanTextOutput(value: string): string | null {
207
- const trimmed = value.trim();
208
- return trimmed ? trimmed.slice(0, 4_000) : null;
209
- }
210
-
211
- interface ParseStreamOptions {
212
- requestId?: string | null;
213
- scope: string;
214
- maxImages?: number;
215
- onPartialImage?: ((partial: { b64: string; index: number | null | undefined }) => void) | null;
216
- onFinalImage?: FinalImageHandler | null;
217
- }
218
-
219
- async function parseStream(res: Response, {
220
- requestId,
221
- scope,
222
- maxImages = 1,
223
- onPartialImage = null,
224
- onFinalImage = null,
225
- }: ParseStreamOptions) {
226
- const reader = res.body!.getReader();
227
- const decoder = new TextDecoder();
228
- const images: ParsedImage[] = [];
229
- const eventTypes: Record<string, number> = {};
230
- let buffer = "";
231
- let usage: Record<string, number> | null = null;
232
- let textOutput = "";
233
- let finalTextOutput: string | null = null;
234
- let webSearchCalls = 0;
235
- let eventCount = 0;
236
- let extraIgnored = 0;
237
- while (true) {
238
- const { done, value } = await reader.read();
239
- if (done) break;
240
- buffer += decoder.decode(value, { stream: true });
241
- let boundary;
242
- while ((boundary = buffer.indexOf("\n\n")) !== -1) {
243
- const block = buffer.slice(0, boundary);
244
- buffer = buffer.slice(boundary + 2);
245
- const eventData = extractSseData(block);
246
- if (!eventData || eventData === "[DONE]") continue;
247
- let data: SseData;
248
- try { data = JSON.parse(eventData); } catch { continue; }
249
- eventCount++;
250
- eventTypes[data.type || "_unknown"] = (eventTypes[data.type || "_unknown"] || 0) + 1;
251
- const delta = extractTextDelta(data);
252
- if (delta) textOutput += delta;
253
- const finalText = extractFinalText(data);
254
- if (finalText) finalTextOutput = finalText;
255
- const partial = extractPartialImage(data);
256
- if (partial && typeof onPartialImage === "function") onPartialImage(partial);
257
- if (data.type === "response.output_item.done" && data.item?.type === "image_generation_call") {
258
- if (data.item.result && images.length < maxImages) {
259
- const image = {
260
- b64: data.item.result,
261
- revisedPrompt: typeof data.item.revised_prompt === "string" ? data.item.revised_prompt : null,
262
- };
263
- const index = images.length;
264
- images.push(image);
265
- if (requestId) setJobPhase(requestId, "decoding");
266
- await onFinalImage?.(image, index);
267
- } else if (data.item.result) extraIgnored++;
268
- }
269
- if (data.type === "response.output_item.done" && data.item?.type === "web_search_call") webSearchCalls++;
270
- if (data.type === "response.completed") {
271
- usage = data.response?.usage || null;
272
- const wsNum = data.response?.tool_usage?.web_search?.num_requests;
273
- if (typeof wsNum === "number" && wsNum > webSearchCalls) webSearchCalls = wsNum;
274
- }
275
- if (data.type === "error") {
276
- throw makeError("Responses stream returned an error", {
277
- code: data.error?.code || "RESPONSES_STREAM_ERROR",
278
- eventCount,
279
- eventType: data.type,
280
- });
281
- }
282
- }
283
- }
284
- logEvent(scope, "stream_end", { requestId, events: eventCount, imageCount: images.length });
285
- return { images, usage, webSearchCalls, eventCount, eventTypes, extraIgnored, text: finalTextOutput ?? cleanTextOutput(textOutput) };
286
- }
287
-
288
- async function parseJson(res: Response, maxImages: number) {
289
- const json = await res.json() as {
290
- output?: Array<{
291
- type?: string;
292
- result?: string;
293
- revised_prompt?: string;
294
- text?: string;
295
- content?: Array<{ type?: string; text?: string }>;
296
- }>;
297
- usage?: Record<string, number>;
298
- };
299
- const images: ParsedImage[] = [];
300
- const textParts: string[] = [];
301
- let webSearchCalls = 0;
302
- for (const item of json.output || []) {
303
- if (item.type === "image_generation_call" && item.result && images.length < maxImages) {
304
- images.push({
305
- b64: item.result,
306
- revisedPrompt: typeof item.revised_prompt === "string" ? item.revised_prompt : null,
307
- });
308
- }
309
- if (item.type === "web_search_call") webSearchCalls++;
310
- const itemText = extractJsonItemText(item);
311
- if (itemText) textParts.push(itemText);
312
- }
313
- return { images, usage: json.usage || null, webSearchCalls, eventCount: 0, eventTypes: {}, extraIgnored: 0, text: cleanTextOutput(textParts.join("\n\n")) };
314
- }
315
-
316
175
  interface PostResponsesArgs {
317
176
  ctx: RouteRuntimeContext;
318
177
  provider: string | undefined;
@@ -400,7 +259,13 @@ async function postResponses({
400
259
  }
401
260
  throw makeError("Responses image generation timed out", { status: 504, code: "RESPONSES_IMAGE_TIMEOUT", cause: err.raw });
402
261
  }
403
- throw err.raw;
262
+ if (isKnownResponsesError(err.raw)) throw err.raw;
263
+ throw makeError("Responses request failed before receiving a response", {
264
+ status: 502,
265
+ code: "NETWORK_FAILED",
266
+ errorName: err.name,
267
+ upstreamMessageRedacted: true,
268
+ });
404
269
  } finally {
405
270
  clearTimeout(timer);
406
271
  }
@@ -418,11 +283,17 @@ interface GenerateOptions {
418
283
  references?: ReferenceRef[];
419
284
  mask?: string;
420
285
  signal?: AbortSignal | null;
286
+ forceImageToolChoice?: boolean;
287
+ allowPromptOnlyOAuthFallback?: boolean;
421
288
  }
422
289
 
423
290
  export async function generateViaResponses(provider: string | undefined, prompt: string | undefined, quality: string | undefined, size: string | undefined, moderation: string = "low", references: ReferenceRef[] = [], requestId: string | null = null, mode: string = "auto", ctxRaw: RouteRuntimeContext = {}, options: GenerateOptions = {}) {
424
291
  const ctx = requireRuntimeContext(ctxRaw);
292
+ const model = options.model || ctx.config?.imageModels?.default || "gpt-5.4-mini";
425
293
  const webSearchEnabled = options.webSearchEnabled !== false && options.searchMode !== "off";
294
+ const requestTools = tools(webSearchEnabled, { quality, size, moderation, ...(options.partialImages ? { partial_images: options.partialImages } : {}) });
295
+ const toolChoice = imageToolChoice(options.forceImageToolChoice ?? ctx.config?.oauth?.forceImageToolChoice !== false);
296
+ const toolChoiceKind = imageToolChoiceKind(toolChoice);
426
297
  const referenceInputs = references.map(normalizeRef);
427
298
  const userContent = referenceInputs.length
428
299
  ? [...referenceInputs, { type: "input_text", text: buildUserTextPrompt(prompt, mode, { webSearchEnabled }) }]
@@ -437,26 +308,62 @@ export async function generateViaResponses(provider: string | undefined, prompt:
437
308
  onPartialImage: options.onPartialImage,
438
309
  onFinalImage: options.onFinalImage,
439
310
  payload: {
440
- model: options.model || ctx.config?.imageModels?.default || "gpt-5.4-mini",
311
+ model,
441
312
  input: [
442
313
  { role: "developer", content: webSearchEnabled ? GENERATE_DEVELOPER_PROMPT : GENERATE_NO_SEARCH_DEVELOPER_PROMPT },
443
314
  { role: "user", content: userContent },
444
315
  ],
445
- tools: tools(webSearchEnabled, { quality, size, moderation, ...(options.partialImages ? { partial_images: options.partialImages } : {}) }),
446
- tool_choice: "required",
316
+ tools: requestTools,
317
+ tool_choice: toolChoice,
447
318
  reasoning: { effort: options.reasoningEffort || "low" },
448
319
  stream: true,
449
320
  },
450
321
  });
451
322
  const image = result.images[0];
452
- if (!image?.b64) throw makeError("No image data received from Responses API", { code: "EMPTY_RESPONSE", eventCount: result.eventCount });
323
+ if (!image?.b64) {
324
+ if (options.allowPromptOnlyOAuthFallback === true) {
325
+ const fallback = await retryPromptOnlyJsonImage({
326
+ postResponses,
327
+ ctx,
328
+ provider,
329
+ prompt,
330
+ mode,
331
+ model,
332
+ quality,
333
+ size,
334
+ moderation,
335
+ requestId,
336
+ signal: options.signal,
337
+ initial: result,
338
+ referencesDroppedOnRetry: referenceInputs.length > 0,
339
+ webSearchDroppedOnRetry: webSearchEnabled,
340
+ reasoningEffort: options.reasoningEffort,
341
+ });
342
+ if (fallback) return fallback;
343
+ }
344
+ throw emptyResponseError("No image data received from Responses API", result, {
345
+ provider,
346
+ model,
347
+ quality,
348
+ size,
349
+ moderation,
350
+ webSearchEnabled,
351
+ refsCount: referenceInputs.length,
352
+ inputImageCount: referenceInputs.length,
353
+ promptChars: typeof prompt === "string" ? prompt.length : 0,
354
+ toolTypes: toolTypes(requestTools),
355
+ toolChoiceKind,
356
+ });
357
+ }
453
358
  return { b64: image.b64, usage: result.usage, webSearchCalls: result.webSearchCalls, revisedPrompt: image.revisedPrompt, text: result.text };
454
359
  }
455
360
 
456
361
  export async function generateMultimodeViaResponses(provider: string | undefined, prompt: string | undefined, quality: string | undefined, size: string | undefined, moderation: string = "low", references: ReferenceRef[] = [], requestId: string | null = null, mode: string = "auto", ctxRaw: RouteRuntimeContext = {}, options: GenerateOptions = {}) {
457
362
  const ctx = requireRuntimeContext(ctxRaw);
458
363
  const maxImages = Math.min(8, Math.max(1, Math.trunc(Number(options.maxImages) || 1)));
364
+ const model = options.model || ctx.config?.imageModels?.default || "gpt-5.4-mini";
459
365
  const webSearchEnabled = options.webSearchEnabled !== false && options.searchMode !== "off";
366
+ const requestTools = tools(webSearchEnabled, { quality, size, moderation, ...(options.partialImages ? { partial_images: options.partialImages } : {}) });
460
367
  const userText = buildMultimodeSequencePrompt(
461
368
  mode === "direct"
462
369
  ? `${prompt}${DIRECT_PROMPT_FIDELITY_SUFFIX}`
@@ -478,12 +385,12 @@ export async function generateMultimodeViaResponses(provider: string | undefined
478
385
  onPartialImage: options.onPartialImage,
479
386
  onFinalImage: options.onFinalImage,
480
387
  payload: {
481
- model: options.model || ctx.config?.imageModels?.default || "gpt-5.4-mini",
388
+ model,
482
389
  input: [
483
390
  { role: "developer", content: webSearchEnabled ? MULTIMODE_DEVELOPER_PROMPT : MULTIMODE_NO_SEARCH_DEVELOPER_PROMPT },
484
391
  { role: "user", content: userContent },
485
392
  ],
486
- tools: tools(webSearchEnabled, { quality, size, moderation, ...(options.partialImages ? { partial_images: options.partialImages } : {}) }),
393
+ tools: requestTools,
487
394
  tool_choice: "required",
488
395
  reasoning: { effort: options.reasoningEffort || "low" },
489
396
  stream: true,
@@ -493,7 +400,11 @@ export async function generateMultimodeViaResponses(provider: string | undefined
493
400
 
494
401
  export async function editViaResponses(provider: string | undefined, prompt: string | undefined, imageB64: string | undefined, quality: string | undefined, size: string | undefined, moderation: string = "low", mode: string = "auto", ctxRaw: RouteRuntimeContext = {}, requestId: string | null = null, options: GenerateOptions = {}) {
495
402
  const ctx = requireRuntimeContext(ctxRaw);
403
+ const model = options.model || ctx.config?.imageModels?.default || "gpt-5.4-mini";
496
404
  const webSearchEnabled = options.webSearchEnabled !== false && options.searchMode !== "off";
405
+ const requestTools = tools(webSearchEnabled, { quality, size, moderation });
406
+ const toolChoice = imageToolChoice(options.forceImageToolChoice ?? ctx.config?.oauth?.forceImageToolChoice !== false);
407
+ const toolChoiceKind = imageToolChoiceKind(toolChoice);
497
408
  const imageForRequest = await compressReferenceB64ForOAuth(imageB64, {
498
409
  maxB64Bytes: ctx.config?.limits?.maxRefB64Bytes,
499
410
  force: true,
@@ -524,18 +435,32 @@ export async function editViaResponses(provider: string | undefined, prompt: str
524
435
  maxImages: 1,
525
436
  signal: options.signal,
526
437
  payload: {
527
- model: options.model || ctx.config?.imageModels?.default || "gpt-5.4-mini",
438
+ model,
528
439
  input: [
529
440
  { role: "developer", content: webSearchEnabled ? EDIT_DEVELOPER_PROMPT : EDIT_NO_SEARCH_DEVELOPER_PROMPT },
530
441
  { role: "user", content: userContent },
531
442
  ],
532
- tools: tools(webSearchEnabled, { quality, size, moderation }),
533
- tool_choice: "required",
443
+ tools: requestTools,
444
+ tool_choice: toolChoice,
534
445
  reasoning: { effort: options.reasoningEffort || "low" },
535
446
  stream: true,
536
447
  },
537
448
  });
538
449
  const image = result.images[0];
539
- if (!image?.b64) throw makeError("No image data received from Responses edit", { code: "EMPTY_RESPONSE", eventCount: result.eventCount });
450
+ if (!image?.b64) {
451
+ throw emptyResponseError("No image data received from Responses edit", result, {
452
+ provider,
453
+ model,
454
+ quality,
455
+ size,
456
+ moderation,
457
+ webSearchEnabled,
458
+ refsCount: referenceImages.length,
459
+ inputImageCount: 1 + referenceImages.length + (maskContent.length ? 1 : 0),
460
+ promptChars: typeof prompt === "string" ? prompt.length : 0,
461
+ toolTypes: toolTypes(requestTools),
462
+ toolChoiceKind,
463
+ });
464
+ }
540
465
  return { b64: image.b64, usage: result.usage, revisedPrompt: image.revisedPrompt, webSearchCalls: result.webSearchCalls };
541
466
  }