pi-openai-toolkit 0.14.4 → 0.14.6
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 +119 -147
- package/README.zh.md +119 -151
- package/docs/internals.md +42 -21
- package/package.json +127 -127
- package/src/auto-mode/classifier.ts +165 -165
- package/src/auto-mode/policy.ts +146 -146
- package/src/auto-mode/prompt.ts +279 -279
- package/src/auto-mode/reviewer.ts +325 -325
- package/src/auto-mode/transcript.ts +133 -133
- package/src/auto-mode/types.ts +137 -137
- package/src/codex-astra/effort-planner.ts +175 -175
- package/src/codex-astra/extension.ts +122 -122
- package/src/config.ts +621 -592
- package/src/context-management/codex-provider.ts +260 -260
- package/src/context-management/history-notes.ts +357 -325
- package/src/context-management/messages.ts +112 -106
- package/src/context-management/namespace-tools.ts +426 -426
- package/src/context-management/tool-contract.ts +23 -23
- package/src/context-management/tools.ts +306 -311
- package/src/context-management/types.ts +168 -161
- package/src/context-management/window-budget.ts +83 -83
- package/src/context-management/window-manager.ts +60 -51
- package/src/context-management/window-request.ts +125 -125
- package/src/debug.ts +1 -1
- package/src/extension-runtime.ts +34 -9
- package/src/image-generation/artifacts.ts +237 -237
- package/src/image-generation/client.ts +248 -248
- package/src/image-generation/eligibility.ts +18 -18
- package/src/image-generation/extension.ts +19 -2
- package/src/image-generation/protocol.ts +473 -405
- package/src/image-generation/references.ts +194 -194
- package/src/image-generation/render.ts +252 -252
- package/src/image-generation/service.ts +249 -243
- package/src/image-generation/types.ts +189 -184
- package/src/model-scope.ts +17 -17
- package/src/provider-headers.ts +23 -23
- package/src/responses-headers.ts +173 -108
- package/src/runtime.ts +513 -477
- package/src/types.ts +11 -0
- package/src/web-search/extension.ts +80 -80
- package/src/web-search/payload.ts +96 -96
- package/src/web-search/prompt.ts +20 -20
- package/src/web-search/types.ts +45 -45
|
@@ -1,405 +1,473 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
IMAGE_GENERATION_QUALITIES,
|
|
5
|
-
IMAGE_GENERATION_SIZES,
|
|
6
|
-
MAX_GENERATED_IMAGE_BYTES,
|
|
7
|
-
MAX_IMAGE_DIAGNOSTIC_CHARS,
|
|
8
|
-
MAX_IMAGE_DIMENSION,
|
|
9
|
-
MAX_IMAGE_IDENTIFIER_CHARS,
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
type
|
|
17
|
-
type
|
|
18
|
-
type
|
|
19
|
-
type
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
):
|
|
125
|
-
|
|
126
|
-
if (
|
|
127
|
-
throw new ImageGenerationError(
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
return
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
if (
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
if (
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
}
|
|
1
|
+
import { DEFAULT_IMAGE_GENERATION_MODELS } from "../types";
|
|
2
|
+
import {
|
|
3
|
+
IMAGE_GENERATION_MIME_TYPE,
|
|
4
|
+
IMAGE_GENERATION_QUALITIES,
|
|
5
|
+
IMAGE_GENERATION_SIZES,
|
|
6
|
+
MAX_GENERATED_IMAGE_BYTES,
|
|
7
|
+
MAX_IMAGE_DIAGNOSTIC_CHARS,
|
|
8
|
+
MAX_IMAGE_DIMENSION,
|
|
9
|
+
MAX_IMAGE_IDENTIFIER_CHARS,
|
|
10
|
+
MAX_IMAGE_MODEL_ID_CHARS,
|
|
11
|
+
MAX_IMAGE_PATH_CHARS,
|
|
12
|
+
MAX_IMAGE_PROMPT_CHARS,
|
|
13
|
+
MAX_REFERENCE_IMAGE_COUNT,
|
|
14
|
+
ImageGenerationError,
|
|
15
|
+
sanitizeImageDiagnostic,
|
|
16
|
+
type GenerateImageParams,
|
|
17
|
+
type NormalizedGenerateImageParams,
|
|
18
|
+
type ParsedGeneratedImage,
|
|
19
|
+
type PreparedReferenceImage,
|
|
20
|
+
type ReferenceImageMimeType,
|
|
21
|
+
} from "./types";
|
|
22
|
+
|
|
23
|
+
const MAX_DISPLAYED_IMAGE_MODELS_IN_ERROR = 5;
|
|
24
|
+
const PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
25
|
+
const BASE64_PATTERN = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
|
|
26
|
+
const DATA_URL_PATTERN = /^data:image\/[a-z0-9.+-]+;base64,(.*)$/is;
|
|
27
|
+
|
|
28
|
+
export type ImageGenerationRequestBody = {
|
|
29
|
+
model: string;
|
|
30
|
+
store: false;
|
|
31
|
+
stream: false;
|
|
32
|
+
parallel_tool_calls: false;
|
|
33
|
+
input: Array<{
|
|
34
|
+
role: "user";
|
|
35
|
+
content: Array<
|
|
36
|
+
| { type: "input_text"; text: string }
|
|
37
|
+
| { type: "input_image"; image_url: string; detail: "auto" }
|
|
38
|
+
>;
|
|
39
|
+
}>;
|
|
40
|
+
tools: Array<{
|
|
41
|
+
type: "image_generation";
|
|
42
|
+
model: string;
|
|
43
|
+
action: "generate" | "edit";
|
|
44
|
+
size: NormalizedGenerateImageParams["size"];
|
|
45
|
+
quality: NormalizedGenerateImageParams["quality"];
|
|
46
|
+
output_format: "png";
|
|
47
|
+
}>;
|
|
48
|
+
tool_choice: { type: "image_generation" };
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export type ParsedImageResponseResult =
|
|
52
|
+
| { ok: true; image: ParsedGeneratedImage }
|
|
53
|
+
| {
|
|
54
|
+
ok: false;
|
|
55
|
+
reason: "malformed-response" | "no-image" | "request-rejected" | "oversized-response";
|
|
56
|
+
errorMessage: string;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
60
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function normalizedOptionalString(value: unknown, maxChars: number): string | undefined {
|
|
64
|
+
if (typeof value !== "string") return undefined;
|
|
65
|
+
const trimmed = value.trim();
|
|
66
|
+
return trimmed ? trimmed.slice(0, maxChars) : undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function normalizePathList(value: unknown): string[] {
|
|
70
|
+
if (value === null || value === undefined) return [];
|
|
71
|
+
if (!Array.isArray(value)) {
|
|
72
|
+
throw new ImageGenerationError(
|
|
73
|
+
"invalid-parameters",
|
|
74
|
+
"referenceImagePaths must be an array of path strings or null.",
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const normalized: string[] = [];
|
|
79
|
+
for (const rawPath of value) {
|
|
80
|
+
if (typeof rawPath !== "string") {
|
|
81
|
+
throw new ImageGenerationError(
|
|
82
|
+
"invalid-parameters",
|
|
83
|
+
"referenceImagePaths must contain only path strings.",
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
const trimmed = rawPath.trim();
|
|
87
|
+
if (!trimmed) continue;
|
|
88
|
+
if (trimmed.length > MAX_IMAGE_PATH_CHARS) {
|
|
89
|
+
throw new ImageGenerationError(
|
|
90
|
+
"invalid-parameters",
|
|
91
|
+
`Each reference image path must be at most ${MAX_IMAGE_PATH_CHARS} characters.`,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
normalized.push(trimmed);
|
|
95
|
+
if (normalized.length > MAX_REFERENCE_IMAGE_COUNT) {
|
|
96
|
+
throw new ImageGenerationError(
|
|
97
|
+
"invalid-parameters",
|
|
98
|
+
`referenceImagePaths must contain at most ${MAX_REFERENCE_IMAGE_COUNT} paths.`,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return normalized;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function normalizeOutputPath(value: unknown): string | undefined {
|
|
106
|
+
if (value === null || value === undefined) return undefined;
|
|
107
|
+
if (typeof value !== "string") {
|
|
108
|
+
throw new ImageGenerationError(
|
|
109
|
+
"invalid-parameters",
|
|
110
|
+
"outputPath must be a path string or null.",
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
const trimmed = value.trim();
|
|
114
|
+
if (!trimmed) return undefined;
|
|
115
|
+
if (trimmed.length > MAX_IMAGE_PATH_CHARS) {
|
|
116
|
+
throw new ImageGenerationError(
|
|
117
|
+
"invalid-parameters",
|
|
118
|
+
`outputPath must be at most ${MAX_IMAGE_PATH_CHARS} characters when provided.`,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
return trimmed;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function normalizeRequestedModel(value: unknown): string | undefined {
|
|
125
|
+
if (value === null || value === undefined) return undefined;
|
|
126
|
+
if (typeof value !== "string") {
|
|
127
|
+
throw new ImageGenerationError("invalid-parameters", "model must be a model id string or null.");
|
|
128
|
+
}
|
|
129
|
+
const trimmed = value.trim();
|
|
130
|
+
if (!trimmed) return undefined;
|
|
131
|
+
if (trimmed.length > MAX_IMAGE_MODEL_ID_CHARS) {
|
|
132
|
+
throw new ImageGenerationError(
|
|
133
|
+
"invalid-parameters",
|
|
134
|
+
`model must be at most ${MAX_IMAGE_MODEL_ID_CHARS} characters when provided.`,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
return trimmed;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Configured model ids are only trusted as far as the shared bounds: unusable entries are
|
|
142
|
+
* dropped, and an empty or malformed list falls back to the shipped default model.
|
|
143
|
+
*/
|
|
144
|
+
function usableImageModels(models: readonly string[] | undefined): string[] {
|
|
145
|
+
const usable: string[] = [];
|
|
146
|
+
for (const rawModel of Array.isArray(models) ? models : []) {
|
|
147
|
+
if (typeof rawModel !== "string") continue;
|
|
148
|
+
const trimmed = rawModel.trim();
|
|
149
|
+
if (!trimmed || trimmed.length > MAX_IMAGE_MODEL_ID_CHARS) continue;
|
|
150
|
+
if (!usable.includes(trimmed)) usable.push(trimmed);
|
|
151
|
+
}
|
|
152
|
+
return usable.length > 0 ? usable : [...DEFAULT_IMAGE_GENERATION_MODELS];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Resolve the nested `image_generation` model for one call. An omitted request selects the
|
|
157
|
+
* first configured model so configuration order expresses the default; an explicit request must
|
|
158
|
+
* match a configured id exactly, and it fails here — before any paid dispatch.
|
|
159
|
+
*/
|
|
160
|
+
function formatAvailableImageModels(models: readonly string[]): string {
|
|
161
|
+
const visible = models.slice(0, MAX_DISPLAYED_IMAGE_MODELS_IN_ERROR);
|
|
162
|
+
const remaining = models.length - visible.length;
|
|
163
|
+
return remaining > 0 ? `${visible.join(", ")}, … and ${remaining} more` : visible.join(", ");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function selectImageGenerationModel(args: {
|
|
167
|
+
requestedModel?: string;
|
|
168
|
+
configuredModels?: readonly string[];
|
|
169
|
+
}): string {
|
|
170
|
+
const models = usableImageModels(args.configuredModels);
|
|
171
|
+
const requested = typeof args.requestedModel === "string" ? args.requestedModel.trim() : "";
|
|
172
|
+
if (!requested) return models[0]!;
|
|
173
|
+
if (models.includes(requested)) return requested;
|
|
174
|
+
throw new ImageGenerationError(
|
|
175
|
+
"invalid-parameters",
|
|
176
|
+
`Unknown image generation model "${requested.slice(0, MAX_IMAGE_MODEL_ID_CHARS)}". ` +
|
|
177
|
+
`Configure it in imageGeneration.models first; available models: ${formatAvailableImageModels(models)}.`,
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function normalizeGenerateImageParams(
|
|
182
|
+
params: GenerateImageParams,
|
|
183
|
+
): NormalizedGenerateImageParams {
|
|
184
|
+
const prompt = typeof params.prompt === "string" ? params.prompt.trim() : "";
|
|
185
|
+
if (!prompt || prompt.length > MAX_IMAGE_PROMPT_CHARS) {
|
|
186
|
+
throw new ImageGenerationError(
|
|
187
|
+
"invalid-parameters",
|
|
188
|
+
`prompt must be 1-${MAX_IMAGE_PROMPT_CHARS} characters.`,
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const size = params.size ?? "auto";
|
|
193
|
+
if (!(IMAGE_GENERATION_SIZES as readonly string[]).includes(size)) {
|
|
194
|
+
throw new ImageGenerationError(
|
|
195
|
+
"invalid-parameters",
|
|
196
|
+
`Unsupported image size: ${String(size)}.`,
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const quality = params.quality ?? "auto";
|
|
201
|
+
if (!(IMAGE_GENERATION_QUALITIES as readonly string[]).includes(quality)) {
|
|
202
|
+
throw new ImageGenerationError(
|
|
203
|
+
"invalid-parameters",
|
|
204
|
+
`Unsupported image quality: ${String(quality)}.`,
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const referenceImagePaths = normalizePathList(params.referenceImagePaths);
|
|
209
|
+
const outputPath = normalizeOutputPath(params.outputPath);
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
prompt,
|
|
213
|
+
referenceImagePaths,
|
|
214
|
+
outputPath,
|
|
215
|
+
size,
|
|
216
|
+
quality,
|
|
217
|
+
model: normalizeRequestedModel(params.model),
|
|
218
|
+
action: referenceImagePaths.length > 0 ? "edit" : "generate",
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function buildImageGenerationRequest(args: {
|
|
223
|
+
routingModel: string;
|
|
224
|
+
imageModel: string;
|
|
225
|
+
params: NormalizedGenerateImageParams;
|
|
226
|
+
references: readonly PreparedReferenceImage[];
|
|
227
|
+
}): ImageGenerationRequestBody {
|
|
228
|
+
const imageModel = typeof args.imageModel === "string" ? args.imageModel.trim() : "";
|
|
229
|
+
if (!imageModel || imageModel.length > MAX_IMAGE_MODEL_ID_CHARS) {
|
|
230
|
+
throw new ImageGenerationError(
|
|
231
|
+
"invalid-parameters",
|
|
232
|
+
"A configured image generation model is required.",
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
const content: ImageGenerationRequestBody["input"][number]["content"] = [
|
|
236
|
+
{ type: "input_text", text: args.params.prompt },
|
|
237
|
+
];
|
|
238
|
+
for (const reference of args.references) {
|
|
239
|
+
content.push({
|
|
240
|
+
type: "input_image",
|
|
241
|
+
image_url: `data:${reference.mimeType};base64,${reference.bytes.toString("base64")}`,
|
|
242
|
+
detail: "auto",
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return {
|
|
247
|
+
model: args.routingModel,
|
|
248
|
+
store: false,
|
|
249
|
+
stream: false,
|
|
250
|
+
parallel_tool_calls: false,
|
|
251
|
+
input: [{ role: "user", content }],
|
|
252
|
+
tools: [
|
|
253
|
+
{
|
|
254
|
+
type: "image_generation",
|
|
255
|
+
model: imageModel,
|
|
256
|
+
action: args.params.action,
|
|
257
|
+
size: args.params.size,
|
|
258
|
+
quality: args.params.quality,
|
|
259
|
+
output_format: "png",
|
|
260
|
+
},
|
|
261
|
+
],
|
|
262
|
+
tool_choice: { type: "image_generation" },
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function hasPngEndChunk(buffer: Buffer): boolean {
|
|
267
|
+
return (
|
|
268
|
+
buffer.length >= 33 &&
|
|
269
|
+
buffer.readUInt32BE(buffer.length - 12) === 0 &&
|
|
270
|
+
buffer.toString("ascii", buffer.length - 8, buffer.length - 4) === "IEND"
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export function detectReferenceImageMimeType(bytes: Uint8Array): ReferenceImageMimeType | undefined {
|
|
275
|
+
const buffer = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
276
|
+
if (readPngDimensions(buffer) && hasPngEndChunk(buffer)) return "image/png";
|
|
277
|
+
if (
|
|
278
|
+
buffer.length >= 4 &&
|
|
279
|
+
buffer[0] === 0xff &&
|
|
280
|
+
buffer[1] === 0xd8 &&
|
|
281
|
+
buffer[2] === 0xff &&
|
|
282
|
+
buffer.lastIndexOf(Buffer.from([0xff, 0xd9])) >= 3
|
|
283
|
+
) {
|
|
284
|
+
return "image/jpeg";
|
|
285
|
+
}
|
|
286
|
+
if (
|
|
287
|
+
buffer.length >= 20 &&
|
|
288
|
+
buffer.toString("ascii", 0, 4) === "RIFF" &&
|
|
289
|
+
buffer.toString("ascii", 8, 12) === "WEBP"
|
|
290
|
+
) {
|
|
291
|
+
const declaredSize = buffer.readUInt32LE(4) + 8;
|
|
292
|
+
const chunkType = buffer.toString("ascii", 12, 16);
|
|
293
|
+
const chunkSize = buffer.readUInt32LE(16);
|
|
294
|
+
const paddedChunkSize = chunkSize + (chunkSize % 2);
|
|
295
|
+
if (
|
|
296
|
+
declaredSize === buffer.length &&
|
|
297
|
+
(chunkType === "VP8 " || chunkType === "VP8L" || chunkType === "VP8X") &&
|
|
298
|
+
20 + paddedChunkSize <= buffer.length
|
|
299
|
+
) {
|
|
300
|
+
return "image/webp";
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return undefined;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export function isValidPng(bytes: Uint8Array): boolean {
|
|
307
|
+
return bytes.byteLength >= PNG_SIGNATURE.length && Buffer.from(bytes).subarray(0, 8).equals(PNG_SIGNATURE);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export function readPngDimensions(bytes: Uint8Array): { width: number; height: number } | undefined {
|
|
311
|
+
const buffer = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
312
|
+
if (!isValidPng(buffer) || buffer.length < 24) return undefined;
|
|
313
|
+
if (buffer.readUInt32BE(8) !== 13 || buffer.toString("ascii", 12, 16) !== "IHDR") return undefined;
|
|
314
|
+
const width = buffer.readUInt32BE(16);
|
|
315
|
+
const height = buffer.readUInt32BE(20);
|
|
316
|
+
if (
|
|
317
|
+
width <= 0 ||
|
|
318
|
+
height <= 0 ||
|
|
319
|
+
width > MAX_IMAGE_DIMENSION ||
|
|
320
|
+
height > MAX_IMAGE_DIMENSION
|
|
321
|
+
) return undefined;
|
|
322
|
+
return { width, height };
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function extractBase64(value: string): string {
|
|
326
|
+
const trimmed = value.trim();
|
|
327
|
+
const dataUrl = DATA_URL_PATTERN.exec(trimmed);
|
|
328
|
+
return (dataUrl?.[1] ?? trimmed).trim();
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export function decodeGeneratedPng(value: string):
|
|
332
|
+
| { ok: true; bytes: Buffer; width: number; height: number }
|
|
333
|
+
| { ok: false; reason: "malformed-response" | "oversized-response"; errorMessage: string } {
|
|
334
|
+
const base64 = extractBase64(value);
|
|
335
|
+
const maxBase64Chars = Math.ceil(MAX_GENERATED_IMAGE_BYTES / 3) * 4;
|
|
336
|
+
if (base64.length > maxBase64Chars) {
|
|
337
|
+
return {
|
|
338
|
+
ok: false,
|
|
339
|
+
reason: "oversized-response",
|
|
340
|
+
errorMessage: "Image generation returned an image larger than the 32 MiB limit.",
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
if (!base64 || base64.length % 4 !== 0 || !BASE64_PATTERN.test(base64)) {
|
|
344
|
+
return {
|
|
345
|
+
ok: false,
|
|
346
|
+
reason: "malformed-response",
|
|
347
|
+
errorMessage: "Image generation returned invalid base64 image data.",
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const bytes = Buffer.from(base64, "base64");
|
|
352
|
+
if (
|
|
353
|
+
bytes.length === 0 ||
|
|
354
|
+
bytes.length > MAX_GENERATED_IMAGE_BYTES ||
|
|
355
|
+
bytes.toString("base64") !== base64
|
|
356
|
+
) {
|
|
357
|
+
bytes.fill(0);
|
|
358
|
+
return {
|
|
359
|
+
ok: false,
|
|
360
|
+
reason: bytes.length > MAX_GENERATED_IMAGE_BYTES ? "oversized-response" : "malformed-response",
|
|
361
|
+
errorMessage: "Image generation returned invalid or oversized image data.",
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
const dimensions = readPngDimensions(bytes);
|
|
365
|
+
if (!dimensions) {
|
|
366
|
+
bytes.fill(0);
|
|
367
|
+
return {
|
|
368
|
+
ok: false,
|
|
369
|
+
reason: "malformed-response",
|
|
370
|
+
errorMessage: "Image generation returned data that is not a valid PNG image.",
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
return { ok: true, bytes, ...dimensions };
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
export function extractProviderErrorMessage(value: unknown, fallback: string): string {
|
|
377
|
+
if (!isRecord(value)) return fallback;
|
|
378
|
+
const direct = normalizedOptionalString(value.message, MAX_IMAGE_DIAGNOSTIC_CHARS);
|
|
379
|
+
if (direct) return sanitizeImageDiagnostic(direct, fallback);
|
|
380
|
+
if (isRecord(value.error)) {
|
|
381
|
+
const nested = normalizedOptionalString(value.error.message, MAX_IMAGE_DIAGNOSTIC_CHARS);
|
|
382
|
+
if (nested) return sanitizeImageDiagnostic(nested, fallback);
|
|
383
|
+
}
|
|
384
|
+
return fallback;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
export function parseImageGenerationResponse(value: unknown): ParsedImageResponseResult {
|
|
388
|
+
if (!isRecord(value)) {
|
|
389
|
+
return {
|
|
390
|
+
ok: false,
|
|
391
|
+
reason: "malformed-response",
|
|
392
|
+
errorMessage: "Image generation returned a non-object response.",
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (value.status !== "completed") {
|
|
397
|
+
return {
|
|
398
|
+
ok: false,
|
|
399
|
+
reason: "request-rejected",
|
|
400
|
+
errorMessage: extractProviderErrorMessage(
|
|
401
|
+
value,
|
|
402
|
+
`Image generation did not complete (status: ${String(value.status ?? "unknown")}).`,
|
|
403
|
+
),
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
if (!Array.isArray(value.output)) {
|
|
407
|
+
return {
|
|
408
|
+
ok: false,
|
|
409
|
+
reason: "malformed-response",
|
|
410
|
+
errorMessage: "Image generation response did not contain an output array.",
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const calls = new Map<string, { result: string; revisedPrompt?: string }>();
|
|
415
|
+
for (let index = 0; index < value.output.length; index += 1) {
|
|
416
|
+
const item = value.output[index];
|
|
417
|
+
if (!isRecord(item) || item.type !== "image_generation_call") continue;
|
|
418
|
+
if (item.status !== "completed") continue;
|
|
419
|
+
const result = typeof item.result === "string"
|
|
420
|
+
? item.result
|
|
421
|
+
: typeof item.b64_json === "string"
|
|
422
|
+
? item.b64_json
|
|
423
|
+
: undefined;
|
|
424
|
+
if (!result?.trim()) continue;
|
|
425
|
+
const id = normalizedOptionalString(item.id, MAX_IMAGE_IDENTIFIER_CHARS) ?? `image_generation_${index}`;
|
|
426
|
+
const revisedPrompt = normalizedOptionalString(item.revised_prompt, MAX_IMAGE_DIAGNOSTIC_CHARS);
|
|
427
|
+
const existing = calls.get(id);
|
|
428
|
+
if (existing && existing.result !== result) {
|
|
429
|
+
return {
|
|
430
|
+
ok: false,
|
|
431
|
+
reason: "malformed-response",
|
|
432
|
+
errorMessage: "Image generation returned conflicting results for one image call.",
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
calls.set(id, { result, revisedPrompt: revisedPrompt ?? existing?.revisedPrompt });
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if (calls.size === 0) {
|
|
439
|
+
return {
|
|
440
|
+
ok: false,
|
|
441
|
+
reason: "no-image",
|
|
442
|
+
errorMessage: "Image generation completed without a usable image result.",
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
if (calls.size !== 1) {
|
|
446
|
+
return {
|
|
447
|
+
ok: false,
|
|
448
|
+
reason: "malformed-response",
|
|
449
|
+
errorMessage: "Image generation returned more than one image result for a single-image request.",
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const [imageCallId, call] = [...calls.entries()][0]!;
|
|
454
|
+
const decoded = decodeGeneratedPng(call.result);
|
|
455
|
+
if (!decoded.ok) return decoded;
|
|
456
|
+
|
|
457
|
+
return {
|
|
458
|
+
ok: true,
|
|
459
|
+
image: {
|
|
460
|
+
bytes: decoded.bytes,
|
|
461
|
+
imageCallId,
|
|
462
|
+
responseId: normalizedOptionalString(value.id, MAX_IMAGE_IDENTIFIER_CHARS),
|
|
463
|
+
revisedPrompt: call.revisedPrompt,
|
|
464
|
+
width: decoded.width,
|
|
465
|
+
height: decoded.height,
|
|
466
|
+
},
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
export const _protocolTest = {
|
|
471
|
+
PNG_SIGNATURE,
|
|
472
|
+
IMAGE_GENERATION_MIME_TYPE,
|
|
473
|
+
};
|