@cartanova/qgrid-cli 2.10.5 → 2.10.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/bundle/dist/application/monit/monit.frame.js +1 -1
- package/bundle/dist/application/qgrid/expired-token-reminder.js +1 -1
- package/bundle/dist/application/qgrid/oauth.js +1 -1
- package/bundle/dist/application/qgrid/qgrid-image-generation.js +7 -11
- package/bundle/dist/application/qgrid/qgrid-response-format.js +4 -4
- package/bundle/dist/application/qgrid/qgrid-run-lifecycle.js +24 -7
- package/bundle/dist/application/qgrid/qgrid.dispatcher.js +8 -3
- package/bundle/dist/application/qgrid/qgrid.frame.js +24 -10
- package/bundle/dist/application/qgrid/token-death.js +1 -1
- package/bundle/dist/application/qgrid/token-window-keepalive.js +1 -1
- package/bundle/dist/application/qgrid/tool-emulation.js +1 -1
- package/bundle/dist/application/request-log/request-log.model.js +2 -2
- package/bundle/dist/i18n/sd.generated.js +3 -3
- package/bundle/dist/sonamu.config.js +2 -2
- package/bundle/dist/utils/providers/anthropic/anthropic-dispatcher.js +1 -1
- package/bundle/dist/utils/providers/anthropic/anthropic-thinking.js +1 -1
- package/bundle/dist/utils/providers/openai/openai-backend-protocol.js +13 -2
- package/bundle/dist/utils/providers/openai/openai-direct-client.js +5 -4
- package/bundle/dist/utils/providers/openai/openai-dispatcher.js +111 -57
- package/bundle/dist/utils/providers/openai/openai-image-generation.js +25 -17
- package/bundle/dist/utils/providers/openai/openai-refresh.js +1 -1
- package/bundle/src/application/qgrid/qgrid-image-generation.test.ts +19 -8
- package/bundle/src/application/qgrid/qgrid-image-generation.ts +17 -24
- package/bundle/src/application/qgrid/qgrid-response-format.test.ts +4 -2
- package/bundle/src/application/qgrid/qgrid-response-format.ts +7 -10
- package/bundle/src/application/qgrid/qgrid-run-lifecycle.test.ts +87 -9
- package/bundle/src/application/qgrid/qgrid-run-lifecycle.ts +45 -5
- package/bundle/src/application/qgrid/qgrid.dispatcher.test.ts +136 -2
- package/bundle/src/application/qgrid/qgrid.dispatcher.ts +11 -1
- package/bundle/src/application/qgrid/qgrid.frame.test.ts +73 -0
- package/bundle/src/application/qgrid/qgrid.frame.ts +39 -4
- package/bundle/src/application/request-log/request-log.entity.json +2 -2
- package/bundle/src/application/sonamu.generated.http +2 -2
- package/bundle/src/i18n/sd.generated.ts +2 -2
- package/bundle/src/utils/providers/openai/openai-backend-protocol.test.ts +23 -1
- package/bundle/src/utils/providers/openai/openai-backend-protocol.ts +24 -2
- package/bundle/src/utils/providers/openai/openai-direct-client.test.ts +15 -0
- package/bundle/src/utils/providers/openai/openai-direct-client.ts +14 -4
- package/bundle/src/utils/providers/openai/openai-dispatcher.test.ts +223 -32
- package/bundle/src/utils/providers/openai/openai-dispatcher.ts +174 -72
- package/bundle/src/utils/providers/openai/openai-image-generation.test.ts +89 -0
- package/bundle/src/utils/providers/openai/openai-image-generation.ts +26 -15
- package/bundle/web-dist/client/assets/cost-DH2jZbrw.js +1 -0
- package/bundle/web-dist/client/assets/{index-Dxw7DiXF.js → index-D9ryIyL-.js} +4 -4
- package/bundle/web-dist/client/assets/logs-CLf2VNfk.js +1 -0
- package/bundle/web-dist/client/assets/{monit-E8eGW1Sj.js → monit-DojxOLj6.js} +1 -1
- package/bundle/web-dist/client/assets/{routes-7spLfIGZ.js → routes-DXJhNvbI.js} +2 -2
- package/bundle/web-dist/client/assets/{services.generated-jnPHYMER.js → services.generated-DqFYgWsY.js} +1 -1
- package/bundle/web-dist/client/assets/{settings-BdosZFNC.js → settings-bopyz9pa.js} +1 -1
- package/bundle/web-dist/client/assets/{show-b2PFNI3D.js → show-DbLD3MzB.js} +1 -1
- package/bundle/web-dist/client/assets/{tokens-CBdHb2_U.js → tokens-2-AIi9sZ.js} +1 -1
- package/bundle/web-dist/client/assets/{use-oauth-login-flow-Cyn3YPqW.js → use-oauth-login-flow-CnqTWs_H.js} +1 -1
- package/bundle/web-dist/client/assets/{use-update-token-mutation-DQCwCYfR.js → use-update-token-mutation-Cxb9K0fM.js} +1 -1
- package/bundle/web-dist/client/index.html +2 -2
- package/bundle/web-dist/server/assets/{cost-59jBdx-I.js → cost-eEHeTRT5.js} +4 -5
- package/bundle/web-dist/server/assets/{lazyRouteComponent-CUR-kWzI.js → lazyRouteComponent-Bi0fYGLs.js} +2 -2
- package/bundle/web-dist/server/assets/{logs-CaaWO9-Z.js → logs-CIVFxfoi.js} +10 -9
- package/bundle/web-dist/server/assets/{monit-CBil92fJ.js → monit-DBXezg7R.js} +4 -4
- package/bundle/web-dist/server/assets/{plus-Bvdj8Pwx.js → plus-BH-t4cm_.js} +1 -1
- package/bundle/web-dist/server/assets/{react-dom-CyuShs9K.js → react-dom-CZ2eynd8.js} +1 -1
- package/bundle/web-dist/server/assets/{refresh-cw-3QAiSWBd.js → refresh-cw-CosAdcz0.js} +1 -1
- package/bundle/web-dist/server/assets/{routes-DtVf9yqo.js → routes-COWYIvA-.js} +6 -7
- package/bundle/web-dist/server/assets/{services.generated-FPwsG9p3.js → services.generated-607iwhMI.js} +2 -2
- package/bundle/web-dist/server/assets/{settings-DeHNwoMv.js → settings-C_6dpK5p.js} +3 -3
- package/bundle/web-dist/server/assets/{show-8NNdRxhM.js → show-DY396-LD.js} +8 -20
- package/bundle/web-dist/server/assets/{tokens-odgS2-9q.js → tokens-COvV7qUP.js} +4 -4
- package/bundle/web-dist/server/assets/{use-oauth-login-flow-DTQKdYBO.js → use-oauth-login-flow-VYCYf3uv.js} +1 -1
- package/bundle/web-dist/server/assets/{use-update-token-mutation-xvXbPrCI.js → use-update-token-mutation-D8XYV5of.js} +1 -1
- package/bundle/web-dist/server/entry-server.generated.js +10 -10
- package/package.json +1 -1
- package/skills/qgrid/SKILL.md +9 -8
- package/skills/qgrid/references/ai-sdk-provider-contract.md +25 -16
- package/skills/qgrid/references/decision-rationale.md +8 -8
- package/skills/qgrid/references/openai-codex-runtime.md +19 -11
- package/skills/qgrid/references/request-log-run-lifecycle.md +4 -2
- package/skills/qgrid/references/sonamu-api-web-flow.md +2 -2
- package/skills/qgrid/references/verification-and-debugging.md +23 -9
- package/bundle/web-dist/client/assets/cost-CY2BJEPC.js +0 -1
- package/bundle/web-dist/client/assets/logs-3n_U4qnI.js +0 -1
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
import { SmoothWeightedRoundRobin } from "../common/smooth-weighted-round-robin";
|
|
15
15
|
import { ThinkingValidationError } from "../common/thinking";
|
|
16
16
|
import {
|
|
17
|
+
imageMimeType,
|
|
17
18
|
type JsonValue,
|
|
18
19
|
type OpenAIResponseItem,
|
|
19
20
|
type OpenAIResponsesOptions,
|
|
@@ -34,16 +35,25 @@ const THINKING_OFF_MODELS = new Set([
|
|
|
34
35
|
"gpt-5.5",
|
|
35
36
|
]);
|
|
36
37
|
const DEFAULT_REQUEST_TIMEOUT_MS = 600_000;
|
|
37
|
-
const
|
|
38
|
+
const IMAGE_TOOL = "generate_image";
|
|
39
|
+
|
|
40
|
+
type ImageToolArguments = {
|
|
41
|
+
prompt: string;
|
|
42
|
+
transparent_background: boolean;
|
|
43
|
+
num_last_images_to_include: number;
|
|
44
|
+
};
|
|
38
45
|
|
|
39
46
|
export type ImageFailureKind = "gate" | "not_called" | "incomplete";
|
|
40
47
|
|
|
41
48
|
export class ImageGenerationError extends Error {
|
|
49
|
+
completedDriver?: GenerateResult;
|
|
50
|
+
|
|
42
51
|
constructor(
|
|
43
52
|
readonly kind: ImageFailureKind,
|
|
44
53
|
message: string,
|
|
54
|
+
options?: ErrorOptions,
|
|
45
55
|
) {
|
|
46
|
-
super(message);
|
|
56
|
+
super(message, options);
|
|
47
57
|
this.name = "ImageGenerationError";
|
|
48
58
|
}
|
|
49
59
|
}
|
|
@@ -104,6 +114,26 @@ function inputItems(req: GenerateRequest): OpenAIResponseItem[] {
|
|
|
104
114
|
return [...history, { type: "message", role: "user", content }];
|
|
105
115
|
}
|
|
106
116
|
|
|
117
|
+
function imageReferences(req: GenerateRequest): OpenAIResponseItem[] {
|
|
118
|
+
return inputItems(req).flatMap((item) => {
|
|
119
|
+
if (item.type === "image_generation_call" && typeof item.result === "string") {
|
|
120
|
+
return [
|
|
121
|
+
{
|
|
122
|
+
type: "input_image",
|
|
123
|
+
image_url: `data:${imageMimeType(item.result, item.output_format)};base64,${item.result}`,
|
|
124
|
+
},
|
|
125
|
+
];
|
|
126
|
+
}
|
|
127
|
+
if (!Array.isArray(item.content)) return [];
|
|
128
|
+
return item.content.filter(
|
|
129
|
+
(part): part is OpenAIResponseItem =>
|
|
130
|
+
part !== null &&
|
|
131
|
+
typeof part === "object" &&
|
|
132
|
+
(part.type === "input_image" || part.type === "image_url"),
|
|
133
|
+
);
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
107
137
|
function asReasoning(req: GenerateRequest): OpenAIResponsesOptions["reasoning"] | undefined {
|
|
108
138
|
if (req.thinking === false) return { effort: "none" };
|
|
109
139
|
const summary =
|
|
@@ -130,6 +160,9 @@ function requestOptions(req: GenerateRequest): OpenAIResponsesOptions {
|
|
|
130
160
|
"Automatic image generation does not support structured output or external tools",
|
|
131
161
|
);
|
|
132
162
|
}
|
|
163
|
+
if (auto && outputFormat && outputFormat !== "png") {
|
|
164
|
+
throw new ImageGenerationError("gate", "Automatic image generation only supports PNG output");
|
|
165
|
+
}
|
|
133
166
|
return {
|
|
134
167
|
model: req.model ?? "",
|
|
135
168
|
...(req.systemPrompt ? { instructions: req.systemPrompt } : {}),
|
|
@@ -137,7 +170,7 @@ function requestOptions(req: GenerateRequest): OpenAIResponsesOptions {
|
|
|
137
170
|
? {
|
|
138
171
|
instructions: [
|
|
139
172
|
req.systemPrompt,
|
|
140
|
-
"Use
|
|
173
|
+
"Use generate_image when image generation or editing is needed; otherwise reply with text. Call it at most once per response. Supply a complete, self-contained prompt with all relevant conversation instructions. Set transparent_background to true only when a transparent background is requested. Set num_last_images_to_include to 0 for a new image without references. For editing or reference-based generation, choose the smallest number of recent conversation images that includes all needed references, at most 5. References retain their chronological order. Do not select images for an unrelated new request.",
|
|
141
174
|
]
|
|
142
175
|
.filter(Boolean)
|
|
143
176
|
.join("\n\n"),
|
|
@@ -145,20 +178,24 @@ function requestOptions(req: GenerateRequest): OpenAIResponsesOptions {
|
|
|
145
178
|
tools: [
|
|
146
179
|
{
|
|
147
180
|
type: "function",
|
|
148
|
-
name:
|
|
181
|
+
name: IMAGE_TOOL,
|
|
149
182
|
description:
|
|
150
|
-
"Generate or edit an image with a transparent background
|
|
151
|
-
strict
|
|
183
|
+
"Generate or edit an image, optionally with a transparent background or recent conversation images as references.",
|
|
184
|
+
// Astra's subscription backend rejects strict tools; validate arguments locally.
|
|
185
|
+
strict: false,
|
|
152
186
|
parameters: {
|
|
153
187
|
type: "object",
|
|
154
188
|
properties: {
|
|
155
189
|
prompt: {
|
|
156
190
|
type: "string",
|
|
191
|
+
minLength: 1,
|
|
157
192
|
description:
|
|
158
|
-
"Complete instructions for the
|
|
193
|
+
"Complete, self-contained instructions for the image generation or edit.",
|
|
159
194
|
},
|
|
195
|
+
transparent_background: { type: "boolean" },
|
|
196
|
+
num_last_images_to_include: { type: "integer", minimum: 0, maximum: 5 },
|
|
160
197
|
},
|
|
161
|
-
required: ["prompt"],
|
|
198
|
+
required: ["prompt", "transparent_background", "num_last_images_to_include"],
|
|
162
199
|
additionalProperties: false,
|
|
163
200
|
},
|
|
164
201
|
},
|
|
@@ -173,12 +210,11 @@ function requestOptions(req: GenerateRequest): OpenAIResponsesOptions {
|
|
|
173
210
|
: {}),
|
|
174
211
|
...(req.promptCacheKey ? { promptCacheKey: req.promptCacheKey } : {}),
|
|
175
212
|
...(req.outputSchema ? { outputSchema: { schema: req.outputSchema as JsonValue } } : {}),
|
|
176
|
-
...(req.imageGeneration
|
|
213
|
+
...(req.imageGeneration && !auto
|
|
177
214
|
? {
|
|
178
215
|
imageGeneration: req.imageGenerationOptions
|
|
179
216
|
? {
|
|
180
217
|
...imageControls,
|
|
181
|
-
...(auto ? { background: "auto" } : {}),
|
|
182
218
|
...(outputFormat ? { output_format: outputFormat } : {}),
|
|
183
219
|
}
|
|
184
220
|
: true,
|
|
@@ -368,12 +404,22 @@ export class OpenAIDispatcher implements ProviderDispatcher {
|
|
|
368
404
|
const images: GeneratedImage[] = [];
|
|
369
405
|
let imageAttempted = false;
|
|
370
406
|
const auto = req.imageGeneration === "auto";
|
|
371
|
-
const
|
|
372
|
-
const
|
|
373
|
-
let
|
|
407
|
+
const imageCalls = new Map<string, ImageToolArguments>();
|
|
408
|
+
const imageCallIds = new Set<unknown>();
|
|
409
|
+
let imageToolAttempted = false;
|
|
374
410
|
let driverCompleted = false;
|
|
375
411
|
let servingModel = req.model ?? "";
|
|
376
412
|
const metadata = selection.metadata;
|
|
413
|
+
const driverResult = (): GenerateResult => ({
|
|
414
|
+
text,
|
|
415
|
+
tokenName: metadata.name,
|
|
416
|
+
usage,
|
|
417
|
+
durationMs: Date.now() - startedAt,
|
|
418
|
+
ttftMs: firstDeltaAt === undefined ? null : firstDeltaAt - startedAt,
|
|
419
|
+
model: servingModel,
|
|
420
|
+
...(servingModel !== req.model ? { requestedModel: req.model } : {}),
|
|
421
|
+
threadCoord: { workerId: selection.tokenId, threadId: req.promptCacheKey ?? "", epoch: -1 },
|
|
422
|
+
});
|
|
377
423
|
const clientEntry = this.acquireClient(selection);
|
|
378
424
|
try {
|
|
379
425
|
for await (const event of clientEntry.client.responses(
|
|
@@ -428,6 +474,8 @@ export class OpenAIDispatcher implements ProviderDispatcher {
|
|
|
428
474
|
streamedMessageIds.add(event.item.id);
|
|
429
475
|
}
|
|
430
476
|
} else if (event.type === "image") {
|
|
477
|
+
if (auto)
|
|
478
|
+
throw new ImageGenerationError("incomplete", "Unexpected hosted image generation");
|
|
431
479
|
imageAttempted = true;
|
|
432
480
|
images.push({
|
|
433
481
|
data: event.base64,
|
|
@@ -436,20 +484,22 @@ export class OpenAIDispatcher implements ProviderDispatcher {
|
|
|
436
484
|
...(event.generation ? { generation: event.generation } : {}),
|
|
437
485
|
});
|
|
438
486
|
} else if (event.type === "output-item" && event.item.type === "image_generation_call") {
|
|
487
|
+
if (auto)
|
|
488
|
+
throw new ImageGenerationError("incomplete", "Unexpected hosted image generation");
|
|
439
489
|
imageAttempted = true;
|
|
440
490
|
} else if (auto && event.type === "output-item" && event.item.type === "function_call") {
|
|
441
|
-
if (event.item.name !==
|
|
491
|
+
if (event.item.name !== IMAGE_TOOL) {
|
|
442
492
|
throw new ImageGenerationError("incomplete", "Unexpected automatic image tool call");
|
|
443
493
|
}
|
|
444
|
-
|
|
445
|
-
|
|
494
|
+
imageToolAttempted = true;
|
|
495
|
+
imageCallIds.add(event.item.call_id);
|
|
446
496
|
if (!event.completed) continue;
|
|
447
497
|
const id = event.item.call_id;
|
|
448
498
|
let args: unknown;
|
|
449
499
|
try {
|
|
450
500
|
args = JSON.parse(String(event.item.arguments));
|
|
451
501
|
} catch {
|
|
452
|
-
throw new ImageGenerationError("incomplete", "Invalid
|
|
502
|
+
throw new ImageGenerationError("incomplete", "Invalid image tool arguments");
|
|
453
503
|
}
|
|
454
504
|
if (
|
|
455
505
|
typeof id !== "string" ||
|
|
@@ -457,67 +507,111 @@ export class OpenAIDispatcher implements ProviderDispatcher {
|
|
|
457
507
|
!args ||
|
|
458
508
|
typeof args !== "object" ||
|
|
459
509
|
Array.isArray(args) ||
|
|
460
|
-
Object.keys(args).length !==
|
|
510
|
+
Object.keys(args).length !== 3 ||
|
|
461
511
|
!("prompt" in args) ||
|
|
462
512
|
typeof args.prompt !== "string" ||
|
|
463
|
-
!args.prompt.trim()
|
|
513
|
+
!args.prompt.trim() ||
|
|
514
|
+
!("transparent_background" in args) ||
|
|
515
|
+
typeof args.transparent_background !== "boolean" ||
|
|
516
|
+
!("num_last_images_to_include" in args) ||
|
|
517
|
+
typeof args.num_last_images_to_include !== "number" ||
|
|
518
|
+
!Number.isInteger(args.num_last_images_to_include) ||
|
|
519
|
+
args.num_last_images_to_include < 0 ||
|
|
520
|
+
args.num_last_images_to_include > 5
|
|
464
521
|
) {
|
|
465
|
-
throw new ImageGenerationError("incomplete", "Invalid
|
|
522
|
+
throw new ImageGenerationError("incomplete", "Invalid image tool arguments");
|
|
466
523
|
}
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
524
|
+
const previous = imageCalls.get(id);
|
|
525
|
+
if (
|
|
526
|
+
previous &&
|
|
527
|
+
(previous.prompt !== args.prompt ||
|
|
528
|
+
previous.transparent_background !== args.transparent_background ||
|
|
529
|
+
previous.num_last_images_to_include !== args.num_last_images_to_include)
|
|
530
|
+
) {
|
|
531
|
+
throw new ImageGenerationError("incomplete", "Conflicting image tool calls");
|
|
472
532
|
}
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
totalTokens: event.usage.totalTokens,
|
|
479
|
-
inputTokens: event.usage.inputTokens,
|
|
480
|
-
outputTokens: event.usage.outputTokens,
|
|
481
|
-
cachedInputTokens: event.usage.cachedInputTokens,
|
|
482
|
-
reasoningOutputTokens: event.usage.reasoningTokens,
|
|
483
|
-
};
|
|
533
|
+
imageCalls.set(id, {
|
|
534
|
+
prompt: args.prompt,
|
|
535
|
+
transparent_background: args.transparent_background,
|
|
536
|
+
num_last_images_to_include: args.num_last_images_to_include,
|
|
537
|
+
});
|
|
484
538
|
} else if (event.type === "completed") {
|
|
539
|
+
const imageUsage = event.imageGeneration?.usage;
|
|
540
|
+
// The backend also reports all-zero image usage for text-only responses.
|
|
541
|
+
if (
|
|
542
|
+
auto &&
|
|
543
|
+
imageUsage &&
|
|
544
|
+
(imageUsage.input_tokens > 0 ||
|
|
545
|
+
imageUsage.output_tokens > 0 ||
|
|
546
|
+
imageUsage.total_tokens > 0)
|
|
547
|
+
)
|
|
548
|
+
throw new ImageGenerationError("incomplete", "Unexpected hosted image generation");
|
|
485
549
|
driverCompleted = true;
|
|
486
550
|
servingModel = event.model ?? servingModel;
|
|
551
|
+
if (event.usage) {
|
|
552
|
+
usage = {
|
|
553
|
+
totalTokens: event.usage.totalTokens,
|
|
554
|
+
inputTokens: event.usage.inputTokens,
|
|
555
|
+
outputTokens: event.usage.outputTokens,
|
|
556
|
+
cachedInputTokens: event.usage.cachedInputTokens,
|
|
557
|
+
reasoningOutputTokens: event.usage.reasoningTokens,
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
if (event.imageGeneration) {
|
|
561
|
+
const hostedImages = images.filter(
|
|
562
|
+
(image) => image.generation?.route === "codex-responses",
|
|
563
|
+
);
|
|
564
|
+
for (const [index, image] of hostedImages.entries()) {
|
|
565
|
+
image.generation = {
|
|
566
|
+
route: "codex-responses",
|
|
567
|
+
...(event.imageGeneration.model ? { model: event.imageGeneration.model } : {}),
|
|
568
|
+
...image.generation,
|
|
569
|
+
// Tool usage covers the whole response, not each returned image.
|
|
570
|
+
...(index === 0 && event.imageGeneration.usage
|
|
571
|
+
? { usage: event.imageGeneration.usage }
|
|
572
|
+
: {}),
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
}
|
|
487
576
|
} else if (event.type === "error") {
|
|
488
577
|
if (req.imageGeneration)
|
|
489
578
|
throw new ImageGenerationError("incomplete", event.error.message);
|
|
490
579
|
throw event.error;
|
|
491
580
|
}
|
|
492
581
|
}
|
|
493
|
-
if (auto &&
|
|
494
|
-
if (
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
582
|
+
if (auto && imageToolAttempted) {
|
|
583
|
+
if (!driverCompleted || imageCalls.size !== 1 || imageCallIds.size !== 1) {
|
|
584
|
+
throw new ImageGenerationError("incomplete", "Expected one completed image tool call");
|
|
585
|
+
}
|
|
586
|
+
if (req.abortSignal?.aborted) throw abortError(req.abortSignal);
|
|
587
|
+
const args = [...imageCalls.values()][0]!;
|
|
588
|
+
const referenceCount = args.num_last_images_to_include;
|
|
589
|
+
const references = referenceCount ? imageReferences(req) : [];
|
|
590
|
+
if (referenceCount > references.length) {
|
|
500
591
|
throw new ImageGenerationError(
|
|
501
592
|
"incomplete",
|
|
502
|
-
"
|
|
593
|
+
"Requested image references are unavailable",
|
|
503
594
|
);
|
|
504
595
|
}
|
|
505
|
-
if (req.abortSignal?.aborted) throw abortError(req.abortSignal);
|
|
506
|
-
const prompt = [...transparentCalls.values()][0]!;
|
|
507
596
|
let imageCompleted = false;
|
|
508
597
|
for await (const event of clientEntry.client.responses(
|
|
509
598
|
{
|
|
510
|
-
model:
|
|
511
|
-
...(req.systemPrompt ? { instructions: req.systemPrompt } : {}),
|
|
599
|
+
model: "gpt-image-2",
|
|
512
600
|
history: [
|
|
513
|
-
|
|
514
|
-
|
|
601
|
+
{
|
|
602
|
+
type: "message",
|
|
603
|
+
role: "user",
|
|
604
|
+
content: [
|
|
605
|
+
{ type: "input_text", text: args.prompt },
|
|
606
|
+
...(referenceCount ? references.slice(-referenceCount) : []),
|
|
607
|
+
],
|
|
608
|
+
},
|
|
515
609
|
],
|
|
516
610
|
imageGeneration: {
|
|
517
611
|
quality: req.imageGenerationOptions?.quality,
|
|
518
612
|
size: req.imageGenerationOptions?.size,
|
|
519
613
|
output_format: "png",
|
|
520
|
-
background: "transparent",
|
|
614
|
+
background: args.transparent_background ? "transparent" : "opaque",
|
|
521
615
|
},
|
|
522
616
|
},
|
|
523
617
|
req.abortSignal,
|
|
@@ -531,38 +625,46 @@ export class OpenAIDispatcher implements ProviderDispatcher {
|
|
|
531
625
|
});
|
|
532
626
|
else if (event.type === "completed") imageCompleted = true;
|
|
533
627
|
else if (event.type === "error")
|
|
534
|
-
throw new ImageGenerationError("incomplete", event.error.message
|
|
628
|
+
throw new ImageGenerationError("incomplete", event.error.message, {
|
|
629
|
+
cause: event.error,
|
|
630
|
+
});
|
|
535
631
|
}
|
|
536
632
|
imageAttempted = true;
|
|
537
633
|
if (!imageCompleted)
|
|
538
|
-
throw new ImageGenerationError(
|
|
539
|
-
"incomplete",
|
|
540
|
-
"Transparent image generation did not complete",
|
|
541
|
-
);
|
|
634
|
+
throw new ImageGenerationError("incomplete", "Image generation did not complete");
|
|
542
635
|
}
|
|
543
636
|
if (auto && imageAttempted && !driverCompleted)
|
|
544
637
|
throw new ImageGenerationError("incomplete", "Image generation response did not complete");
|
|
545
638
|
if (req.abortSignal?.aborted) throw abortError(req.abortSignal);
|
|
639
|
+
if ((req.imageGeneration === true || (auto && imageAttempted)) && images.length === 0) {
|
|
640
|
+
throw new ImageGenerationError(
|
|
641
|
+
imageAttempted ? "incomplete" : "not_called",
|
|
642
|
+
imageAttempted
|
|
643
|
+
? "image generation did not complete"
|
|
644
|
+
: "model did not call image generation",
|
|
645
|
+
);
|
|
646
|
+
}
|
|
647
|
+
} catch (error) {
|
|
648
|
+
if (auto && driverCompleted && imageToolAttempted) {
|
|
649
|
+
const failure =
|
|
650
|
+
error instanceof ImageGenerationError
|
|
651
|
+
? error
|
|
652
|
+
: new ImageGenerationError(
|
|
653
|
+
"incomplete",
|
|
654
|
+
error instanceof Error ? error.message : String(error),
|
|
655
|
+
{ cause: error },
|
|
656
|
+
);
|
|
657
|
+
if (error instanceof Error) failure.name = error.name;
|
|
658
|
+
// Only the completed chat stage is chargeable here; discard partial image output.
|
|
659
|
+
failure.completedDriver = driverResult();
|
|
660
|
+
throw failure;
|
|
661
|
+
}
|
|
662
|
+
throw error;
|
|
546
663
|
} finally {
|
|
547
664
|
this.releaseClient(clientEntry);
|
|
548
665
|
}
|
|
549
|
-
if ((req.imageGeneration === true || (auto && imageAttempted)) && images.length === 0) {
|
|
550
|
-
throw new ImageGenerationError(
|
|
551
|
-
imageAttempted ? "incomplete" : "not_called",
|
|
552
|
-
imageAttempted
|
|
553
|
-
? "image generation did not complete"
|
|
554
|
-
: "model did not call image generation",
|
|
555
|
-
);
|
|
556
|
-
}
|
|
557
666
|
return {
|
|
558
|
-
|
|
559
|
-
tokenName: metadata.name,
|
|
560
|
-
usage,
|
|
561
|
-
durationMs: Date.now() - startedAt,
|
|
562
|
-
ttftMs: firstDeltaAt === undefined ? null : firstDeltaAt - startedAt,
|
|
563
|
-
model: servingModel,
|
|
564
|
-
...(servingModel !== req.model ? { requestedModel: req.model } : {}),
|
|
565
|
-
threadCoord: { workerId: selection.tokenId, threadId: req.promptCacheKey ?? "", epoch: -1 },
|
|
667
|
+
...driverResult(),
|
|
566
668
|
...(images.length ? { images } : {}),
|
|
567
669
|
};
|
|
568
670
|
}
|
|
@@ -150,3 +150,92 @@ describe("standalone transparent images", () => {
|
|
|
150
150
|
expect(fetchMock).not.toHaveBeenCalled();
|
|
151
151
|
});
|
|
152
152
|
});
|
|
153
|
+
|
|
154
|
+
describe("standalone image model", () => {
|
|
155
|
+
it.each([true, { background: "opaque" }, { background: "auto" }])(
|
|
156
|
+
"generates ordinary PNGs directly without the Responses transport (%j)",
|
|
157
|
+
async (imageGeneration) => {
|
|
158
|
+
const stream = vi.fn();
|
|
159
|
+
const fetchMock = vi.fn<typeof fetch>(async () => Response.json({
|
|
160
|
+
data: [{ b64_json: png([255, 255]) }],
|
|
161
|
+
}));
|
|
162
|
+
const client = new OpenAIDirectClient({
|
|
163
|
+
credentials: { accessToken: "subscription-token", accountId: "account" },
|
|
164
|
+
fetch: fetchMock,
|
|
165
|
+
transport: { stream },
|
|
166
|
+
});
|
|
167
|
+
const events = await Array.fromAsync(client.responses({
|
|
168
|
+
...options, model: "gpt-image-2", imageGeneration,
|
|
169
|
+
}));
|
|
170
|
+
expect(stream).not.toHaveBeenCalled();
|
|
171
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
172
|
+
const [url, init] = fetchMock.mock.calls[0]!;
|
|
173
|
+
expect(url).toBe("https://chatgpt.com/backend-api/codex/images/generations");
|
|
174
|
+
expect(init?.headers).toMatchObject({
|
|
175
|
+
Authorization: "Bearer subscription-token", "ChatGPT-Account-ID": "account",
|
|
176
|
+
Accept: "application/json",
|
|
177
|
+
});
|
|
178
|
+
const body = JSON.parse(String(init?.body));
|
|
179
|
+
expect(body.model).toBe("gpt-image-2");
|
|
180
|
+
expect(body.background).toBe(typeof imageGeneration === "object" ? imageGeneration.background : undefined);
|
|
181
|
+
expect(events[0]).toMatchObject({
|
|
182
|
+
type: "image", mimeType: "image/png",
|
|
183
|
+
generation: { route: "codex-images", model: "gpt-image-2", size: "2x1" },
|
|
184
|
+
});
|
|
185
|
+
expect(events[0]).not.toHaveProperty("generation.background");
|
|
186
|
+
expect(events[0]).not.toHaveProperty("generation.quality");
|
|
187
|
+
expect(events[1]).toMatchObject({ type: "completed", model: "gpt-image-2" });
|
|
188
|
+
expect(events[1]).not.toHaveProperty("usage");
|
|
189
|
+
},
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
it("edits references and preserves observed settings and aggregate usage once", async () => {
|
|
193
|
+
const usage = {
|
|
194
|
+
input_tokens: 2260, output_tokens: 1415, total_tokens: 3675,
|
|
195
|
+
input_tokens_details: { text_tokens: 248, image_tokens: 2012 },
|
|
196
|
+
};
|
|
197
|
+
const fetchMock = vi.fn<typeof fetch>(async () => Response.json({
|
|
198
|
+
model: "gpt-image-2-codex", quality: "medium", background: "opaque", size: "1024x1536",
|
|
199
|
+
data: [{ b64_json: png([255, 255]), revised_prompt: "A red circle." }, { b64_json: png() }],
|
|
200
|
+
usage,
|
|
201
|
+
}));
|
|
202
|
+
const events = await run(fetchMock, {
|
|
203
|
+
model: "gpt-image-2",
|
|
204
|
+
imageGeneration: { background: "auto", quality: "high", size: "1024x1536", output_format: "png" },
|
|
205
|
+
history: [{ role: "user", content: [
|
|
206
|
+
{ type: "input_text", text: "Use these references." },
|
|
207
|
+
{ type: "input_image", image_url: "data:image/png;base64,reference" },
|
|
208
|
+
{ type: "input_image", image_url: "https://example.com/reference.png" },
|
|
209
|
+
] }],
|
|
210
|
+
});
|
|
211
|
+
expect(fetchMock.mock.calls[0]?.[0]).toBe("https://chatgpt.com/backend-api/codex/images/edits");
|
|
212
|
+
expect(JSON.parse(String(fetchMock.mock.calls[0]?.[1]?.body))).toMatchObject({
|
|
213
|
+
model: "gpt-image-2", background: "auto", quality: "high", size: "1024x1536",
|
|
214
|
+
images: [{ image_url: "data:image/png;base64,reference" }, { image_url: "https://example.com/reference.png" }],
|
|
215
|
+
});
|
|
216
|
+
expect(events[0]).toMatchObject({
|
|
217
|
+
type: "image", revisedPrompt: "A red circle.",
|
|
218
|
+
generation: { model: "gpt-image-2-codex", background: "opaque", quality: "medium", size: "2x1", usage },
|
|
219
|
+
});
|
|
220
|
+
expect(events[1]).not.toHaveProperty("generation.usage");
|
|
221
|
+
expect(events[2]).toMatchObject({ type: "completed", model: "gpt-image-2-codex" });
|
|
222
|
+
expect(events[2]).not.toHaveProperty("usage");
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it.each([
|
|
226
|
+
{ imageGeneration: false },
|
|
227
|
+
{ imageGeneration: undefined },
|
|
228
|
+
{ imageGeneration: { output_format: "webp" } },
|
|
229
|
+
{ imageGeneration: { output_format: "jpeg" } },
|
|
230
|
+
])("rejects disabled generation and unsupported output formats before I/O (%j)", async (overrides) => {
|
|
231
|
+
const fetchMock = vi.fn<typeof fetch>();
|
|
232
|
+
await expect(run(fetchMock, { model: "gpt-image-2", ...overrides })).rejects.toThrow(/enabled|PNG/);
|
|
233
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it("validates decoded PNG content on the ordinary route", async () => {
|
|
237
|
+
const fetchMock = vi.fn<typeof fetch>(async () => Response.json(result("bm90IHBuZw==")));
|
|
238
|
+
await expect(run(fetchMock, { model: "gpt-image-2", imageGeneration: true })).rejects.toThrow(/PNG/);
|
|
239
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
240
|
+
});
|
|
241
|
+
});
|
|
@@ -18,17 +18,20 @@ const PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
|
18
18
|
export function buildStandaloneImageRequest(options: OpenAIResponsesOptions) {
|
|
19
19
|
if (options.outputSchema || options.tools?.length) {
|
|
20
20
|
throw new OpenAIProtocolError(
|
|
21
|
-
"
|
|
21
|
+
"Standalone image generation does not support tools or structured output",
|
|
22
22
|
);
|
|
23
23
|
}
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
if (!options.imageGeneration) {
|
|
25
|
+
throw new OpenAIProtocolError("Standalone images require image generation to be enabled");
|
|
26
|
+
}
|
|
27
|
+
const controls = typeof options.imageGeneration === "object" ? options.imageGeneration : {};
|
|
28
|
+
if (options.model !== "gpt-image-2" && controls.background !== "transparent") {
|
|
26
29
|
throw new OpenAIProtocolError(
|
|
27
30
|
"Standalone images require an explicit transparent background request",
|
|
28
31
|
);
|
|
29
32
|
}
|
|
30
33
|
if (controls.output_format && controls.output_format !== "png") {
|
|
31
|
-
throw new OpenAIProtocolError("
|
|
34
|
+
throw new OpenAIProtocolError("Standalone image generation only supports PNG output");
|
|
32
35
|
}
|
|
33
36
|
const text: string[] = [];
|
|
34
37
|
const images: Array<{ image_url: string }> = [];
|
|
@@ -43,7 +46,7 @@ export function buildStandaloneImageRequest(options: OpenAIResponsesOptions) {
|
|
|
43
46
|
}
|
|
44
47
|
if (item.type !== "message" && typeof item.role !== "string") {
|
|
45
48
|
throw new OpenAIProtocolError(
|
|
46
|
-
"Unsupported conversation item for
|
|
49
|
+
"Unsupported conversation item for standalone image generation",
|
|
47
50
|
);
|
|
48
51
|
}
|
|
49
52
|
if (typeof item.content === "string") {
|
|
@@ -51,11 +54,11 @@ export function buildStandaloneImageRequest(options: OpenAIResponsesOptions) {
|
|
|
51
54
|
continue;
|
|
52
55
|
}
|
|
53
56
|
if (!Array.isArray(item.content)) {
|
|
54
|
-
throw new OpenAIProtocolError("Unsupported message content for
|
|
57
|
+
throw new OpenAIProtocolError("Unsupported message content for standalone image generation");
|
|
55
58
|
}
|
|
56
59
|
for (const part of item.content) {
|
|
57
60
|
if (!part || typeof part !== "object") {
|
|
58
|
-
throw new OpenAIProtocolError("Unsupported message part for
|
|
61
|
+
throw new OpenAIProtocolError("Unsupported message part for standalone image generation");
|
|
59
62
|
}
|
|
60
63
|
if (
|
|
61
64
|
["input_text", "output_text", "text"].includes(part.type) &&
|
|
@@ -69,12 +72,12 @@ export function buildStandaloneImageRequest(options: OpenAIResponsesOptions) {
|
|
|
69
72
|
}
|
|
70
73
|
images.push({ image_url: url });
|
|
71
74
|
} else {
|
|
72
|
-
throw new OpenAIProtocolError("Unsupported message part for
|
|
75
|
+
throw new OpenAIProtocolError("Unsupported message part for standalone image generation");
|
|
73
76
|
}
|
|
74
77
|
}
|
|
75
78
|
}
|
|
76
79
|
if (images.length > 5)
|
|
77
|
-
throw new OpenAIProtocolError("
|
|
80
|
+
throw new OpenAIProtocolError("Standalone image editing supports at most 5 reference images");
|
|
78
81
|
if (!text.some((part) => part.trim())) throw new OpenAIProtocolError("Image prompt is empty");
|
|
79
82
|
return {
|
|
80
83
|
url: CHATGPT_CODEX_RESPONSES_URL.replace(
|
|
@@ -84,7 +87,7 @@ export function buildStandaloneImageRequest(options: OpenAIResponsesOptions) {
|
|
|
84
87
|
body: {
|
|
85
88
|
model: "gpt-image-2",
|
|
86
89
|
prompt: text.join("\n\n"),
|
|
87
|
-
background:
|
|
90
|
+
...(controls.background ? { background: controls.background } : {}),
|
|
88
91
|
...(controls.quality ? { quality: controls.quality } : {}),
|
|
89
92
|
...(controls.size ? { size: controls.size } : {}),
|
|
90
93
|
...(images.length ? { images } : {}),
|
|
@@ -92,8 +95,10 @@ export function buildStandaloneImageRequest(options: OpenAIResponsesOptions) {
|
|
|
92
95
|
};
|
|
93
96
|
}
|
|
94
97
|
|
|
95
|
-
|
|
96
|
-
|
|
98
|
+
function inspectGeneratedPng(
|
|
99
|
+
base64: string,
|
|
100
|
+
requireTransparency: boolean,
|
|
101
|
+
): { width: number; height: number } {
|
|
97
102
|
if (base64.length > Math.ceil(MAX_PNG_BYTES / 3) * 4 || !/^[A-Za-z0-9+/]+={0,2}$/.test(base64)) {
|
|
98
103
|
throw new OpenAIProtocolError("Invalid or oversized generated PNG");
|
|
99
104
|
}
|
|
@@ -116,6 +121,8 @@ export function inspectTransparentPng(base64: string): { width: number; height:
|
|
|
116
121
|
} catch {
|
|
117
122
|
throw new OpenAIProtocolError("Image generation returned an invalid PNG");
|
|
118
123
|
}
|
|
124
|
+
if (!requireTransparency) return { width: image.width, height: image.height };
|
|
125
|
+
// An alpha channel alone can still be fully opaque or have no visible content.
|
|
119
126
|
let transparent = false;
|
|
120
127
|
let visible = false;
|
|
121
128
|
for (let i = 3; i < image.data.length; i += 4) {
|
|
@@ -130,6 +137,7 @@ export function inspectTransparentPng(base64: string): { width: number; height:
|
|
|
130
137
|
|
|
131
138
|
export async function readStandaloneImageResponse(
|
|
132
139
|
response: Response,
|
|
140
|
+
requireTransparency: boolean,
|
|
133
141
|
): Promise<OpenAINormalizedEvent[]> {
|
|
134
142
|
if (!response.body) throw new OpenAIProtocolError("Image response had no body");
|
|
135
143
|
const reader = response.body.getReader();
|
|
@@ -155,6 +163,8 @@ export async function readStandaloneImageResponse(
|
|
|
155
163
|
.min(1)
|
|
156
164
|
.max(16),
|
|
157
165
|
quality: z.string().optional(),
|
|
166
|
+
model: z.string().optional(),
|
|
167
|
+
background: z.string().optional(),
|
|
158
168
|
usage: ImageGenerationMetadata.shape.usage,
|
|
159
169
|
});
|
|
160
170
|
let parsed: z.infer<typeof schema>;
|
|
@@ -163,8 +173,9 @@ export async function readStandaloneImageResponse(
|
|
|
163
173
|
} catch {
|
|
164
174
|
throw new OpenAIProtocolError("Invalid standalone image response");
|
|
165
175
|
}
|
|
176
|
+
const background = parsed.background ?? (requireTransparency ? "transparent" : undefined);
|
|
166
177
|
return parsed.data.map((item, index) => {
|
|
167
|
-
const dimensions =
|
|
178
|
+
const dimensions = inspectGeneratedPng(item.b64_json, requireTransparency);
|
|
168
179
|
return {
|
|
169
180
|
type: "image",
|
|
170
181
|
base64: item.b64_json,
|
|
@@ -172,8 +183,8 @@ export async function readStandaloneImageResponse(
|
|
|
172
183
|
...(item.revised_prompt ? { revisedPrompt: item.revised_prompt } : {}),
|
|
173
184
|
generation: {
|
|
174
185
|
route: "codex-images",
|
|
175
|
-
model: "gpt-image-2",
|
|
176
|
-
background:
|
|
186
|
+
model: parsed.model ?? "gpt-image-2",
|
|
187
|
+
...(background ? { background } : {}),
|
|
177
188
|
size: `${dimensions.width}x${dimensions.height}`,
|
|
178
189
|
...(parsed.quality ? { quality: parsed.quality } : {}),
|
|
179
190
|
...(index === 0 && parsed.usage ? { usage: parsed.usage } : {}),
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var e=1e6,t=4;function n(t){return t/e}function r(e){return`$${e.toFixed(t)}`}function i(e){return r(n(e))}function a(e){return e.cost_usd===null&&e.image_cost_usd===null?`—`:r(n((e.cost_usd??0)+(e.image_cost_usd??0)))}function o(e){return e.input_tokens<=0?`—`:`${Math.round(e.cache_read_tokens/e.input_tokens*100)}%`}export{r as i,i as n,a as r,o as t};
|