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,114 @@
1
+ import { logEvent } from "./logger.js";
2
+ import type { ParsedResponsesResult } from "./responsesParse.js";
3
+ import type { RouteRuntimeContext } from "./runtimeContext.js";
4
+ import { imageToolChoice, tools } from "./responsesTools.js";
5
+ import { emptyResponseError } from "./responsesErrors.js";
6
+ import { buildUserTextPrompt } from "./oauthProxy.js";
7
+
8
+ type PostResponses = (args: {
9
+ ctx: RouteRuntimeContext;
10
+ provider: string | undefined;
11
+ scope: string;
12
+ payload: unknown;
13
+ requestId?: string | null;
14
+ maxImages?: number;
15
+ signal?: AbortSignal | null;
16
+ }) => Promise<ParsedResponsesResult>;
17
+
18
+ export async function retryPromptOnlyJsonImage({
19
+ postResponses,
20
+ ctx,
21
+ provider,
22
+ prompt,
23
+ mode,
24
+ model,
25
+ quality,
26
+ size,
27
+ moderation,
28
+ requestId,
29
+ signal,
30
+ initial,
31
+ referencesDroppedOnRetry,
32
+ webSearchDroppedOnRetry,
33
+ reasoningEffort,
34
+ }: {
35
+ postResponses: PostResponses;
36
+ ctx: RouteRuntimeContext;
37
+ provider: string | undefined;
38
+ prompt: string | undefined;
39
+ mode: string;
40
+ model: string;
41
+ quality?: string;
42
+ size?: string;
43
+ moderation?: string;
44
+ requestId: string | null;
45
+ signal?: AbortSignal | null;
46
+ initial: ParsedResponsesResult;
47
+ referencesDroppedOnRetry: boolean;
48
+ webSearchDroppedOnRetry: boolean;
49
+ reasoningEffort?: string;
50
+ }) {
51
+ if (provider === "api") return null;
52
+ const retryKind = "prompt_only_json_image_tool";
53
+ const retryMeta = {
54
+ retryKind,
55
+ initialEventCount: initial.eventCount,
56
+ initialEventTypes: initial.eventTypes,
57
+ referencesDroppedOnRetry,
58
+ developerPromptDroppedOnRetry: true,
59
+ webSearchDroppedOnRetry,
60
+ };
61
+ logEvent("oauth", "retry_json", { requestId, ...retryMeta });
62
+ let retry: ParsedResponsesResult;
63
+ try {
64
+ retry = await postResponses({
65
+ ctx,
66
+ provider,
67
+ scope: "oauth-fallback",
68
+ requestId,
69
+ maxImages: 1,
70
+ signal,
71
+ payload: {
72
+ model,
73
+ input: [{ role: "user", content: buildUserTextPrompt(prompt, mode, { webSearchEnabled: false }) }],
74
+ tools: tools(false, { quality, size, moderation }),
75
+ tool_choice: imageToolChoice(true),
76
+ reasoning: { effort: reasoningEffort || "low" },
77
+ stream: false,
78
+ },
79
+ });
80
+ } catch (e) {
81
+ if (e && typeof e === "object") Object.assign(e, retryMeta);
82
+ throw e;
83
+ }
84
+ const image = retry.images[0];
85
+ if (image?.b64) {
86
+ logEvent("oauth", "retry_image", { requestId, retryKind, imageChars: image.b64.length });
87
+ return { b64: image.b64, usage: retry.usage, webSearchCalls: initial.webSearchCalls, revisedPrompt: image.revisedPrompt, text: retry.text, ...retryMeta };
88
+ }
89
+ logEvent("oauth", "retry_no_image", {
90
+ requestId,
91
+ retryKind,
92
+ fallbackEventCount: retry.eventCount,
93
+ fallbackImageCallSeen: retry.diagnostics.imageCallSeen,
94
+ fallbackImageResultCount: retry.diagnostics.imageResultCount,
95
+ });
96
+ throw emptyResponseError("No image data received from Responses API fallback", retry, {
97
+ provider,
98
+ model,
99
+ quality,
100
+ size,
101
+ moderation,
102
+ webSearchEnabled: false,
103
+ refsCount: 0,
104
+ inputImageCount: 0,
105
+ promptChars: typeof prompt === "string" ? prompt.length : 0,
106
+ toolTypes: ["image_generation"],
107
+ toolChoiceKind: "image_generation",
108
+ ...retryMeta,
109
+ fallbackEventCount: retry.eventCount,
110
+ fallbackEventTypes: retry.eventTypes,
111
+ fallbackImageCallSeen: retry.diagnostics.imageCallSeen,
112
+ fallbackImageResultCount: retry.diagnostics.imageResultCount,
113
+ });
114
+ }
@@ -1,11 +1,16 @@
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 { requireRuntimeContext } from "./runtimeContext.js";
8
+ import { parseJson, parseStream, safeDiagnosticLabel, } from "./responsesParse.js";
9
+ import { imageToolChoice, imageToolChoiceKind, tools, toolTypes, } from "./responsesTools.js";
10
+ import { emptyResponseError } from "./responsesErrors.js";
11
+ import { retryPromptOnlyJsonImage } from "./responsesFallback.js";
8
12
  import { AUTO_PROMPT_FIDELITY_SUFFIX, DIRECT_PROMPT_FIDELITY_SUFFIX, EDIT_DEVELOPER_PROMPT, EDIT_NO_SEARCH_DEVELOPER_PROMPT, GENERATE_DEVELOPER_PROMPT, GENERATE_NO_SEARCH_DEVELOPER_PROMPT, MULTIMODE_DEVELOPER_PROMPT, MULTIMODE_NO_SEARCH_DEVELOPER_PROMPT, buildEditTextPrompt, buildMultimodeSequencePrompt, buildUserTextPrompt, waitForOAuthReady, } from "./oauthProxy.js";
13
+ const RESPONSES_ERROR_MARKER = "ima2ResponsesError";
9
14
  function makeError(message, { status = 500, code = "RESPONSES_IMAGE_ERROR", cause, ...rest } = {}) {
10
15
  const err = new Error(message);
11
16
  err.status = status;
@@ -13,6 +18,7 @@ function makeError(message, { status = 500, code = "RESPONSES_IMAGE_ERROR", caus
13
18
  if (cause)
14
19
  err.cause = cause;
15
20
  Object.assign(err, rest);
21
+ Object.defineProperty(err, RESPONSES_ERROR_MARKER, { value: true });
16
22
  return err;
17
23
  }
18
24
  function parseOpenAIErrorBody(text) {
@@ -21,9 +27,9 @@ function parseOpenAIErrorBody(text) {
21
27
  const error = parsed?.error || {};
22
28
  return {
23
29
  message: typeof error.message === "string" && error.message ? error.message : "OpenAI request failed",
24
- code: typeof error.code === "string" ? error.code : null,
25
- type: typeof error.type === "string" ? error.type : null,
26
- param: typeof error.param === "string" ? error.param : null,
30
+ code: safeDiagnosticLabel(error.code),
31
+ type: safeDiagnosticLabel(error.type),
32
+ param: safeDiagnosticLabel(error.param),
27
33
  };
28
34
  }
29
35
  catch {
@@ -54,36 +60,56 @@ function safeUpstreamClientMessage(upstream, status) {
54
60
  return "OpenAI rate limited the image request.";
55
61
  return "OpenAI rejected the image request.";
56
62
  }
63
+ function safeBaseUrl(value) {
64
+ try {
65
+ const parsed = new URL(value);
66
+ parsed.username = "";
67
+ parsed.password = "";
68
+ return parsed.toString().replace(/\/$/, "");
69
+ }
70
+ catch {
71
+ return value.replace(/\/$/, "");
72
+ }
73
+ }
74
+ function apiAuthorizationHeader(apiKey) {
75
+ const key = typeof apiKey === "string" ? apiKey.trim() : "";
76
+ if (!key) {
77
+ throw makeError("API key is required for API provider image generation", {
78
+ status: 401,
79
+ code: "API_KEY_REQUIRED",
80
+ });
81
+ }
82
+ if (/[\u0000-\u001f\u007f]/.test(key)) {
83
+ throw makeError("API key contains invalid characters.", {
84
+ status: 401,
85
+ code: "AUTH_API_KEY_INVALID",
86
+ });
87
+ }
88
+ return `Bearer ${key}`;
89
+ }
90
+ function isKnownResponsesError(value) {
91
+ return Boolean(value &&
92
+ typeof value === "object" &&
93
+ value.ima2ResponsesError === true);
94
+ }
57
95
  async function getEndpoint(ctx, provider, _scope) {
58
96
  if (provider === "api") {
59
- if (!ctx?.apiKey) {
60
- throw makeError("API key is required for API provider image generation", {
61
- status: 401,
62
- code: "API_KEY_REQUIRED",
63
- });
64
- }
65
97
  return {
66
98
  url: "https://api.openai.com/v1/responses",
67
99
  headers: {
68
100
  "Content-Type": "application/json",
69
101
  Accept: "text/event-stream",
70
- Authorization: `Bearer ${ctx.apiKey}`,
102
+ Authorization: apiAuthorizationHeader(ctx.apiKey),
71
103
  },
72
104
  };
73
105
  }
74
106
  await waitForOAuthReady(ctx);
75
107
  const port = ctx?.config?.oauth?.proxyPort || 10531;
76
108
  return {
77
- url: `${ctx?.oauthUrl || `http://127.0.0.1:${port}`}/v1/responses`,
109
+ url: `${safeBaseUrl(ctx?.oauthUrl || `http://127.0.0.1:${port}`)}/v1/responses`,
78
110
  headers: { "Content-Type": "application/json", Accept: "text/event-stream" },
79
111
  };
80
112
  }
81
- function tools(webSearchEnabled, imageOptions) {
82
- return [
83
- ...(webSearchEnabled ? [{ type: "web_search" }] : []),
84
- { type: "image_generation", ...imageOptions },
85
- ];
86
- }
87
113
  function normalizeRef(ref) {
88
114
  const b64 = typeof ref === "string" ? ref : ref?.b64;
89
115
  const detectedMime = typeof ref === "object" && ref?.detectedMime
@@ -97,149 +123,6 @@ function normalizeRef(ref) {
97
123
  : "image/png";
98
124
  return { type: "input_image", image_url: `data:${mime};base64,${b64}` };
99
125
  }
100
- function extractSseData(block) {
101
- let eventData = "";
102
- for (const line of block.split("\n")) {
103
- if (line.startsWith("data: "))
104
- eventData += line.slice(6);
105
- }
106
- return eventData;
107
- }
108
- function extractPartialImage(data) {
109
- if (typeof data?.type !== "string" || !data.type.includes("partial"))
110
- return null;
111
- const item = data.item || {};
112
- const b64 = data.partial_image || data.image || data.result || item.partial_image || item.image || item.result;
113
- if (typeof b64 !== "string" || b64.length === 0)
114
- return null;
115
- const index = Number.isFinite(data.index) ? data.index : Number.isFinite(item.index) ? item.index : null;
116
- return { b64, index };
117
- }
118
- function extractTextDelta(data) {
119
- if (data.type === "response.output_text.delta" && typeof data.delta === "string")
120
- return data.delta;
121
- return null;
122
- }
123
- function extractFinalText(data) {
124
- if (data.type === "response.output_text.done" && typeof data.text === "string")
125
- return cleanTextOutput(data.text);
126
- if (data.type === "response.output_item.done" && data.item?.type === "message") {
127
- return extractJsonItemText(data.item);
128
- }
129
- return null;
130
- }
131
- function extractJsonItemText(item) {
132
- if (item.type === "output_text" && typeof item.text === "string")
133
- return cleanTextOutput(item.text);
134
- if (!Array.isArray(item.content))
135
- return null;
136
- const text = item.content
137
- .filter((part) => part.type === "output_text" && typeof part.text === "string")
138
- .map((part) => part.text)
139
- .join("\n\n");
140
- return cleanTextOutput(text);
141
- }
142
- function cleanTextOutput(value) {
143
- const trimmed = value.trim();
144
- return trimmed ? trimmed.slice(0, 4_000) : null;
145
- }
146
- async function parseStream(res, { requestId, scope, maxImages = 1, onPartialImage = null, onFinalImage = null, }) {
147
- const reader = res.body.getReader();
148
- const decoder = new TextDecoder();
149
- const images = [];
150
- const eventTypes = {};
151
- let buffer = "";
152
- let usage = null;
153
- let textOutput = "";
154
- let finalTextOutput = null;
155
- let webSearchCalls = 0;
156
- let eventCount = 0;
157
- let extraIgnored = 0;
158
- while (true) {
159
- const { done, value } = await reader.read();
160
- if (done)
161
- break;
162
- buffer += decoder.decode(value, { stream: true });
163
- let boundary;
164
- while ((boundary = buffer.indexOf("\n\n")) !== -1) {
165
- const block = buffer.slice(0, boundary);
166
- buffer = buffer.slice(boundary + 2);
167
- const eventData = extractSseData(block);
168
- if (!eventData || eventData === "[DONE]")
169
- continue;
170
- let data;
171
- try {
172
- data = JSON.parse(eventData);
173
- }
174
- catch {
175
- continue;
176
- }
177
- eventCount++;
178
- eventTypes[data.type || "_unknown"] = (eventTypes[data.type || "_unknown"] || 0) + 1;
179
- const delta = extractTextDelta(data);
180
- if (delta)
181
- textOutput += delta;
182
- const finalText = extractFinalText(data);
183
- if (finalText)
184
- finalTextOutput = finalText;
185
- const partial = extractPartialImage(data);
186
- if (partial && typeof onPartialImage === "function")
187
- onPartialImage(partial);
188
- if (data.type === "response.output_item.done" && data.item?.type === "image_generation_call") {
189
- if (data.item.result && images.length < maxImages) {
190
- const image = {
191
- b64: data.item.result,
192
- revisedPrompt: typeof data.item.revised_prompt === "string" ? data.item.revised_prompt : null,
193
- };
194
- const index = images.length;
195
- images.push(image);
196
- if (requestId)
197
- setJobPhase(requestId, "decoding");
198
- await onFinalImage?.(image, index);
199
- }
200
- else if (data.item.result)
201
- extraIgnored++;
202
- }
203
- if (data.type === "response.output_item.done" && data.item?.type === "web_search_call")
204
- webSearchCalls++;
205
- if (data.type === "response.completed") {
206
- usage = data.response?.usage || null;
207
- const wsNum = data.response?.tool_usage?.web_search?.num_requests;
208
- if (typeof wsNum === "number" && wsNum > webSearchCalls)
209
- webSearchCalls = wsNum;
210
- }
211
- if (data.type === "error") {
212
- throw makeError("Responses stream returned an error", {
213
- code: data.error?.code || "RESPONSES_STREAM_ERROR",
214
- eventCount,
215
- eventType: data.type,
216
- });
217
- }
218
- }
219
- }
220
- logEvent(scope, "stream_end", { requestId, events: eventCount, imageCount: images.length });
221
- return { images, usage, webSearchCalls, eventCount, eventTypes, extraIgnored, text: finalTextOutput ?? cleanTextOutput(textOutput) };
222
- }
223
- async function parseJson(res, maxImages) {
224
- const json = await res.json();
225
- const images = [];
226
- const textParts = [];
227
- let webSearchCalls = 0;
228
- for (const item of json.output || []) {
229
- if (item.type === "image_generation_call" && item.result && images.length < maxImages) {
230
- images.push({
231
- b64: item.result,
232
- revisedPrompt: typeof item.revised_prompt === "string" ? item.revised_prompt : null,
233
- });
234
- }
235
- if (item.type === "web_search_call")
236
- webSearchCalls++;
237
- const itemText = extractJsonItemText(item);
238
- if (itemText)
239
- textParts.push(itemText);
240
- }
241
- return { images, usage: json.usage || null, webSearchCalls, eventCount: 0, eventTypes: {}, extraIgnored: 0, text: cleanTextOutput(textParts.join("\n\n")) };
242
- }
243
126
  function combineAbortSignals(signals) {
244
127
  if (signals.length === 1)
245
128
  return signals[0];
@@ -307,7 +190,14 @@ async function postResponses({ ctx, provider, scope, payload, requestId, maxImag
307
190
  }
308
191
  throw makeError("Responses image generation timed out", { status: 504, code: "RESPONSES_IMAGE_TIMEOUT", cause: err.raw });
309
192
  }
310
- throw err.raw;
193
+ if (isKnownResponsesError(err.raw))
194
+ throw err.raw;
195
+ throw makeError("Responses request failed before receiving a response", {
196
+ status: 502,
197
+ code: "NETWORK_FAILED",
198
+ errorName: err.name,
199
+ upstreamMessageRedacted: true,
200
+ });
311
201
  }
312
202
  finally {
313
203
  clearTimeout(timer);
@@ -315,7 +205,11 @@ async function postResponses({ ctx, provider, scope, payload, requestId, maxImag
315
205
  }
316
206
  export async function generateViaResponses(provider, prompt, quality, size, moderation = "low", references = [], requestId = null, mode = "auto", ctxRaw = {}, options = {}) {
317
207
  const ctx = requireRuntimeContext(ctxRaw);
208
+ const model = options.model || ctx.config?.imageModels?.default || "gpt-5.4-mini";
318
209
  const webSearchEnabled = options.webSearchEnabled !== false && options.searchMode !== "off";
210
+ const requestTools = tools(webSearchEnabled, { quality, size, moderation, ...(options.partialImages ? { partial_images: options.partialImages } : {}) });
211
+ const toolChoice = imageToolChoice(options.forceImageToolChoice ?? ctx.config?.oauth?.forceImageToolChoice !== false);
212
+ const toolChoiceKind = imageToolChoiceKind(toolChoice);
319
213
  const referenceInputs = references.map(normalizeRef);
320
214
  const userContent = referenceInputs.length
321
215
  ? [...referenceInputs, { type: "input_text", text: buildUserTextPrompt(prompt, mode, { webSearchEnabled }) }]
@@ -330,26 +224,62 @@ export async function generateViaResponses(provider, prompt, quality, size, mode
330
224
  onPartialImage: options.onPartialImage,
331
225
  onFinalImage: options.onFinalImage,
332
226
  payload: {
333
- model: options.model || ctx.config?.imageModels?.default || "gpt-5.4-mini",
227
+ model,
334
228
  input: [
335
229
  { role: "developer", content: webSearchEnabled ? GENERATE_DEVELOPER_PROMPT : GENERATE_NO_SEARCH_DEVELOPER_PROMPT },
336
230
  { role: "user", content: userContent },
337
231
  ],
338
- tools: tools(webSearchEnabled, { quality, size, moderation, ...(options.partialImages ? { partial_images: options.partialImages } : {}) }),
339
- tool_choice: "required",
232
+ tools: requestTools,
233
+ tool_choice: toolChoice,
340
234
  reasoning: { effort: options.reasoningEffort || "low" },
341
235
  stream: true,
342
236
  },
343
237
  });
344
238
  const image = result.images[0];
345
- if (!image?.b64)
346
- throw makeError("No image data received from Responses API", { code: "EMPTY_RESPONSE", eventCount: result.eventCount });
239
+ if (!image?.b64) {
240
+ if (options.allowPromptOnlyOAuthFallback === true) {
241
+ const fallback = await retryPromptOnlyJsonImage({
242
+ postResponses,
243
+ ctx,
244
+ provider,
245
+ prompt,
246
+ mode,
247
+ model,
248
+ quality,
249
+ size,
250
+ moderation,
251
+ requestId,
252
+ signal: options.signal,
253
+ initial: result,
254
+ referencesDroppedOnRetry: referenceInputs.length > 0,
255
+ webSearchDroppedOnRetry: webSearchEnabled,
256
+ reasoningEffort: options.reasoningEffort,
257
+ });
258
+ if (fallback)
259
+ return fallback;
260
+ }
261
+ throw emptyResponseError("No image data received from Responses API", result, {
262
+ provider,
263
+ model,
264
+ quality,
265
+ size,
266
+ moderation,
267
+ webSearchEnabled,
268
+ refsCount: referenceInputs.length,
269
+ inputImageCount: referenceInputs.length,
270
+ promptChars: typeof prompt === "string" ? prompt.length : 0,
271
+ toolTypes: toolTypes(requestTools),
272
+ toolChoiceKind,
273
+ });
274
+ }
347
275
  return { b64: image.b64, usage: result.usage, webSearchCalls: result.webSearchCalls, revisedPrompt: image.revisedPrompt, text: result.text };
348
276
  }
349
277
  export async function generateMultimodeViaResponses(provider, prompt, quality, size, moderation = "low", references = [], requestId = null, mode = "auto", ctxRaw = {}, options = {}) {
350
278
  const ctx = requireRuntimeContext(ctxRaw);
351
279
  const maxImages = Math.min(8, Math.max(1, Math.trunc(Number(options.maxImages) || 1)));
280
+ const model = options.model || ctx.config?.imageModels?.default || "gpt-5.4-mini";
352
281
  const webSearchEnabled = options.webSearchEnabled !== false && options.searchMode !== "off";
282
+ const requestTools = tools(webSearchEnabled, { quality, size, moderation, ...(options.partialImages ? { partial_images: options.partialImages } : {}) });
353
283
  const userText = buildMultimodeSequencePrompt(mode === "direct"
354
284
  ? `${prompt}${DIRECT_PROMPT_FIDELITY_SUFFIX}`
355
285
  : `${prompt}${webSearchEnabled ? "" : ""}${AUTO_PROMPT_FIDELITY_SUFFIX}`, maxImages, { webSearchEnabled });
@@ -367,12 +297,12 @@ export async function generateMultimodeViaResponses(provider, prompt, quality, s
367
297
  onPartialImage: options.onPartialImage,
368
298
  onFinalImage: options.onFinalImage,
369
299
  payload: {
370
- model: options.model || ctx.config?.imageModels?.default || "gpt-5.4-mini",
300
+ model,
371
301
  input: [
372
302
  { role: "developer", content: webSearchEnabled ? MULTIMODE_DEVELOPER_PROMPT : MULTIMODE_NO_SEARCH_DEVELOPER_PROMPT },
373
303
  { role: "user", content: userContent },
374
304
  ],
375
- tools: tools(webSearchEnabled, { quality, size, moderation, ...(options.partialImages ? { partial_images: options.partialImages } : {}) }),
305
+ tools: requestTools,
376
306
  tool_choice: "required",
377
307
  reasoning: { effort: options.reasoningEffort || "low" },
378
308
  stream: true,
@@ -381,7 +311,11 @@ export async function generateMultimodeViaResponses(provider, prompt, quality, s
381
311
  }
382
312
  export async function editViaResponses(provider, prompt, imageB64, quality, size, moderation = "low", mode = "auto", ctxRaw = {}, requestId = null, options = {}) {
383
313
  const ctx = requireRuntimeContext(ctxRaw);
314
+ const model = options.model || ctx.config?.imageModels?.default || "gpt-5.4-mini";
384
315
  const webSearchEnabled = options.webSearchEnabled !== false && options.searchMode !== "off";
316
+ const requestTools = tools(webSearchEnabled, { quality, size, moderation });
317
+ const toolChoice = imageToolChoice(options.forceImageToolChoice ?? ctx.config?.oauth?.forceImageToolChoice !== false);
318
+ const toolChoiceKind = imageToolChoiceKind(toolChoice);
385
319
  const imageForRequest = await compressReferenceB64ForOAuth(imageB64, {
386
320
  maxB64Bytes: ctx.config?.limits?.maxRefB64Bytes,
387
321
  force: true,
@@ -410,19 +344,32 @@ export async function editViaResponses(provider, prompt, imageB64, quality, size
410
344
  maxImages: 1,
411
345
  signal: options.signal,
412
346
  payload: {
413
- model: options.model || ctx.config?.imageModels?.default || "gpt-5.4-mini",
347
+ model,
414
348
  input: [
415
349
  { role: "developer", content: webSearchEnabled ? EDIT_DEVELOPER_PROMPT : EDIT_NO_SEARCH_DEVELOPER_PROMPT },
416
350
  { role: "user", content: userContent },
417
351
  ],
418
- tools: tools(webSearchEnabled, { quality, size, moderation }),
419
- tool_choice: "required",
352
+ tools: requestTools,
353
+ tool_choice: toolChoice,
420
354
  reasoning: { effort: options.reasoningEffort || "low" },
421
355
  stream: true,
422
356
  },
423
357
  });
424
358
  const image = result.images[0];
425
- if (!image?.b64)
426
- throw makeError("No image data received from Responses edit", { code: "EMPTY_RESPONSE", eventCount: result.eventCount });
359
+ if (!image?.b64) {
360
+ throw emptyResponseError("No image data received from Responses edit", result, {
361
+ provider,
362
+ model,
363
+ quality,
364
+ size,
365
+ moderation,
366
+ webSearchEnabled,
367
+ refsCount: referenceImages.length,
368
+ inputImageCount: 1 + referenceImages.length + (maskContent.length ? 1 : 0),
369
+ promptChars: typeof prompt === "string" ? prompt.length : 0,
370
+ toolTypes: toolTypes(requestTools),
371
+ toolChoiceKind,
372
+ });
373
+ }
427
374
  return { b64: image.b64, usage: result.usage, revisedPrompt: image.revisedPrompt, webSearchCalls: result.webSearchCalls };
428
375
  }