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
|
@@ -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
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({
|
|
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({
|
|
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,
|
package/routes/generate.js
CHANGED
|
@@ -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
|
}
|