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.
- package/README.md +10 -1
- package/bin/commands/doctor.js +195 -0
- package/bin/commands/doctor.ts +202 -0
- package/bin/ima2.js +3 -105
- package/bin/ima2.ts +3 -109
- package/config.js +1 -0
- package/config.ts +5 -0
- package/docs/CLI.md +36 -0
- package/docs/FAQ.ko.md +82 -2
- package/docs/FAQ.md +85 -2
- package/docs/PROMPT_STUDIO.ko.md +111 -0
- package/docs/PROMPT_STUDIO.md +115 -0
- package/docs/README.ko.md +8 -1
- package/docs/migration/runtime-test-inventory.md +6 -1
- package/lib/agentRuntime.js +9 -2
- package/lib/agentRuntime.ts +8 -2
- package/lib/errorClassify.js +1 -1
- package/lib/errorClassify.ts +1 -1
- package/lib/generationErrors.js +121 -23
- package/lib/generationErrors.ts +100 -13
- package/lib/responsesDoctor.js +386 -0
- package/lib/responsesDoctor.ts +456 -0
- package/lib/responsesErrors.js +57 -0
- package/lib/responsesErrors.ts +83 -0
- package/lib/responsesFallback.js +72 -0
- package/lib/responsesFallback.ts +114 -0
- package/lib/responsesImageAdapter.js +121 -174
- package/lib/responsesImageAdapter.ts +136 -211
- package/lib/responsesParse.js +324 -0
- package/lib/responsesParse.ts +452 -0
- package/lib/responsesTools.js +15 -0
- package/lib/responsesTools.ts +28 -0
- package/package.json +1 -1
- package/routes/edit.js +26 -1
- package/routes/edit.ts +26 -1
- package/routes/generate.js +40 -0
- package/routes/generate.ts +47 -0
- package/ui/dist/.vite/manifest.json +12 -12
- package/ui/dist/assets/{AgentWorkspace-BJe9yxPA.js → AgentWorkspace-B6YNOZHi.js} +1 -1
- package/ui/dist/assets/{CardNewsWorkspace-BBLdwzYU.js → CardNewsWorkspace-EFVeg4l_.js} +1 -1
- package/ui/dist/assets/{NodeCanvas-BSZ527J4.js → NodeCanvas-iM6yjHvO.js} +1 -1
- package/ui/dist/assets/{PromptBuilderPanel-Y2VygFc0.js → PromptBuilderPanel-C3GdLDCl.js} +1 -1
- package/ui/dist/assets/{PromptImportDialog-C6lFV-LL.js → PromptImportDialog-DS9vrc_w.js} +2 -2
- package/ui/dist/assets/{PromptImportDiscoverySection-D8YJFhND.js → PromptImportDiscoverySection-DHFEt_FA.js} +1 -1
- package/ui/dist/assets/{PromptImportFolderSection-ywfcQolW.js → PromptImportFolderSection-BQxb1zs5.js} +1 -1
- package/ui/dist/assets/{PromptLibraryPanel-fk4KmrGy.js → PromptLibraryPanel-NhMKVGfU.js} +2 -2
- package/ui/dist/assets/{SettingsWorkspace-DL5vhAHQ.js → SettingsWorkspace-FjKjaDqj.js} +1 -1
- package/ui/dist/assets/index-BAN6lKgf.js +28 -0
- package/ui/dist/assets/{index-BLx55BOg.js → index-BbFZyM92.js} +1 -1
- package/ui/dist/assets/index-DK1faG9Z.css +1 -0
- package/ui/dist/index.html +2 -2
- package/ui/dist/assets/index-ByViUJfx.css +0 -1
- 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:
|
|
63
|
-
type:
|
|
64
|
-
param:
|
|
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:
|
|
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
|
|
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:
|
|
446
|
-
tool_choice:
|
|
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)
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
533
|
-
tool_choice:
|
|
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)
|
|
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
|
}
|