@cartanova/qgrid-cli 2.10.4 → 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 +6 -6
- 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 +25 -11
- package/bundle/dist/application/qgrid/qgrid.types.js +4 -8
- 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 +23 -17
- 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 +31 -9
- 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 +34 -5
- package/bundle/src/application/qgrid/qgrid-image-generation.ts +17 -13
- package/bundle/src/application/qgrid/qgrid-response-format.test.ts +11 -0
- 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 +43 -6
- package/bundle/src/application/qgrid/qgrid.types.ts +14 -4
- package/bundle/src/application/request-log/request-log.entity.json +2 -2
- package/bundle/src/application/request-log/request-log.model.test.ts +17 -12
- package/bundle/src/application/request-log/request-log.model.ts +8 -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 +42 -2
- package/bundle/src/utils/providers/openai/openai-backend-protocol.ts +46 -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 +235 -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-reW8VvJF.js → index-D9ryIyL-.js} +4 -4
- package/bundle/web-dist/client/assets/logs-CLf2VNfk.js +1 -0
- package/bundle/web-dist/client/assets/{monit-DkABv7PG.js → monit-DojxOLj6.js} +1 -1
- package/bundle/web-dist/client/assets/{routes-BRmQ4M3-.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-BHvtycEZ.js → settings-bopyz9pa.js} +1 -1
- package/bundle/web-dist/client/assets/{show-qeTIzgDZ.js → show-DbLD3MzB.js} +1 -1
- package/bundle/web-dist/client/assets/{tokens-5Bep_G-w.js → tokens-2-AIi9sZ.js} +1 -1
- package/bundle/web-dist/client/assets/{use-oauth-login-flow-Cx-QO25M.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-CSH_t17I.js → cost-eEHeTRT5.js} +5 -1
- package/bundle/web-dist/server/assets/{lazyRouteComponent-CUR-kWzI.js → lazyRouteComponent-Bi0fYGLs.js} +2 -2
- package/bundle/web-dist/server/assets/{logs-CagNGTBj.js → logs-CIVFxfoi.js} +11 -8
- package/bundle/web-dist/server/assets/{monit-DSxs4Jnl.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-i-sTqXMX.js → routes-COWYIvA-.js} +5 -6
- 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-C4jk1Hrp.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 +3 -2
- 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 +23 -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-CpjVoAqp.js +0 -1
- package/bundle/web-dist/client/assets/logs-DDIt97D1.js +0 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import os from "node:os";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { ImageGenerationMetadata } from "../../../application/qgrid/qgrid.types";
|
|
4
4
|
import { type OpenAIEffort } from "../common/effort";
|
|
5
5
|
|
|
6
6
|
export const CHATGPT_CODEX_RESPONSES_URL =
|
|
@@ -180,7 +180,13 @@ export type OpenAINormalizedEvent =
|
|
|
180
180
|
generation?: ImageGenerationMetadata;
|
|
181
181
|
}
|
|
182
182
|
| { type: "output-item"; item: OpenAIResponseItem; completed?: boolean }
|
|
183
|
-
| {
|
|
183
|
+
| {
|
|
184
|
+
type: "completed";
|
|
185
|
+
responseId: string;
|
|
186
|
+
usage?: OpenAIUsage;
|
|
187
|
+
model?: string;
|
|
188
|
+
imageGeneration?: ImageGenerationMetadata;
|
|
189
|
+
}
|
|
184
190
|
| { type: "error"; error: OpenAIProtocolError };
|
|
185
191
|
|
|
186
192
|
export class OpenAIProtocolError extends Error {
|
|
@@ -248,11 +254,33 @@ export function normalizeOpenAIEvent(raw: unknown): OpenAINormalizedEvent | unde
|
|
|
248
254
|
) {
|
|
249
255
|
const item = event.item as OpenAIResponseItem;
|
|
250
256
|
if (item.type === "image_generation_call" && typeof item.result === "string") {
|
|
257
|
+
// PNG exposes dimensions in its fixed header. Other formats retain the
|
|
258
|
+
// backend's observed size; never substitute requested image controls.
|
|
259
|
+
const header = Buffer.from(item.result.slice(0, 32), "base64");
|
|
260
|
+
const pngSize =
|
|
261
|
+
header.length >= 24 &&
|
|
262
|
+
header.subarray(0, 8).equals(Buffer.from("89504e470d0a1a0a", "hex")) &&
|
|
263
|
+
header.readUInt32BE(16) > 0 &&
|
|
264
|
+
header.readUInt32BE(20) > 0
|
|
265
|
+
? `${header.readUInt32BE(16)}x${header.readUInt32BE(20)}`
|
|
266
|
+
: undefined;
|
|
267
|
+
const size =
|
|
268
|
+
pngSize ??
|
|
269
|
+
(typeof item.size === "string" && /^[1-9]\d*x[1-9]\d*$/.test(item.size)
|
|
270
|
+
? item.size
|
|
271
|
+
: undefined);
|
|
251
272
|
return {
|
|
252
273
|
type: "image",
|
|
253
274
|
...(typeof item.id === "string" ? { id: item.id } : {}),
|
|
254
275
|
base64: item.result,
|
|
255
276
|
mimeType: imageMimeType(item.result, item.output_format),
|
|
277
|
+
generation: {
|
|
278
|
+
route: "codex-responses",
|
|
279
|
+
...(size ? { size } : {}),
|
|
280
|
+
...(typeof item.model === "string" ? { model: item.model } : {}),
|
|
281
|
+
...(typeof item.quality === "string" ? { quality: item.quality } : {}),
|
|
282
|
+
...(typeof item.background === "string" ? { background: item.background } : {}),
|
|
283
|
+
},
|
|
256
284
|
...(typeof item.revised_prompt === "string"
|
|
257
285
|
? { revisedPrompt: item.revised_prompt }
|
|
258
286
|
: typeof item.revisedPrompt === "string"
|
|
@@ -264,11 +292,27 @@ export function normalizeOpenAIEvent(raw: unknown): OpenAINormalizedEvent | unde
|
|
|
264
292
|
}
|
|
265
293
|
if (type === "response.completed") {
|
|
266
294
|
const response = (event.response ?? {}) as Record<string, unknown>;
|
|
295
|
+
const toolUsage = response.tool_usage as Record<string, unknown> | undefined;
|
|
296
|
+
const parsedImageUsage = ImageGenerationMetadata.shape.usage.safeParse(toolUsage?.image_gen);
|
|
297
|
+
const imageUsage = parsedImageUsage.success ? parsedImageUsage.data : undefined;
|
|
298
|
+
const imageTool = Array.isArray(response.tools)
|
|
299
|
+
? response.tools.find((tool) => tool?.type === "image_generation")
|
|
300
|
+
: undefined;
|
|
301
|
+
const imageModel = typeof imageTool?.model === "string" ? imageTool.model : undefined;
|
|
267
302
|
return {
|
|
268
303
|
type: "completed",
|
|
269
304
|
responseId: String(response.id ?? ""),
|
|
270
305
|
...(response.usage ? { usage: usageFrom(response.usage) } : {}),
|
|
271
306
|
...(typeof response.model === "string" ? { model: response.model } : {}),
|
|
307
|
+
...(imageUsage || imageModel
|
|
308
|
+
? {
|
|
309
|
+
imageGeneration: {
|
|
310
|
+
route: "codex-responses" as const,
|
|
311
|
+
...(imageModel ? { model: imageModel } : {}),
|
|
312
|
+
...(imageUsage ? { usage: imageUsage } : {}),
|
|
313
|
+
},
|
|
314
|
+
}
|
|
315
|
+
: {}),
|
|
272
316
|
};
|
|
273
317
|
}
|
|
274
318
|
if (type === "error" || type === "response.failed" || type === "response.incomplete") {
|
|
@@ -29,6 +29,21 @@ async function collect<T>(iterable: AsyncIterable<T>): Promise<T[]> {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
describe("OpenAI direct HTTPS client", () => {
|
|
32
|
+
it.each([true, { background: "opaque" }])("keeps chat-model images on Responses (%j)", async (imageGeneration) => {
|
|
33
|
+
const fetchMock = vi.fn<typeof fetch>(async () =>
|
|
34
|
+
sse({ type: "response.completed", response: { id: "hosted-image" } }),
|
|
35
|
+
);
|
|
36
|
+
const client = new OpenAIDirectClient({
|
|
37
|
+
credentials: { accessToken: "access", accountId: "acct" }, fetch: fetchMock,
|
|
38
|
+
});
|
|
39
|
+
await collect(client.responses({ model: "gpt-6-luna", history: [], imageGeneration }));
|
|
40
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
41
|
+
expect(fetchMock.mock.calls[0]?.[0]).toBe(CHATGPT_CODEX_RESPONSES_URL);
|
|
42
|
+
expect(JSON.parse(String(fetchMock.mock.calls[0]?.[1]?.body))).toMatchObject({
|
|
43
|
+
model: "gpt-6-luna", tools: [expect.objectContaining({ type: "image_generation" })],
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
32
47
|
it("posts the pinned body and Codex identity without live calls", async () => {
|
|
33
48
|
const fetchMock = vi.fn<typeof fetch>(async () =>
|
|
34
49
|
sse(
|
|
@@ -450,8 +450,9 @@ export class OpenAIDirectClient {
|
|
|
450
450
|
|
|
451
451
|
responses(options: OpenAIResponsesOptions, signal?: AbortSignal): OpenAIEventStream {
|
|
452
452
|
if (
|
|
453
|
-
|
|
454
|
-
options.imageGeneration
|
|
453
|
+
options.model === "gpt-image-2" ||
|
|
454
|
+
(typeof options.imageGeneration === "object" &&
|
|
455
|
+
options.imageGeneration.background === "transparent")
|
|
455
456
|
) {
|
|
456
457
|
return this.standaloneImages(options, signal);
|
|
457
458
|
}
|
|
@@ -498,10 +499,19 @@ export class OpenAIDirectClient {
|
|
|
498
499
|
continue;
|
|
499
500
|
}
|
|
500
501
|
if (!response.ok) throw await responseError(response);
|
|
501
|
-
const images = await readStandaloneImageResponse(
|
|
502
|
+
const images = await readStandaloneImageResponse(
|
|
503
|
+
response,
|
|
504
|
+
typeof options.imageGeneration === "object" &&
|
|
505
|
+
options.imageGeneration.background === "transparent",
|
|
506
|
+
);
|
|
502
507
|
signal?.throwIfAborted();
|
|
503
508
|
yield* images;
|
|
504
|
-
|
|
509
|
+
const firstImage = images[0];
|
|
510
|
+
yield {
|
|
511
|
+
type: "completed",
|
|
512
|
+
responseId: requestId,
|
|
513
|
+
model: firstImage?.type === "image" ? firstImage.generation?.model : "gpt-image-2",
|
|
514
|
+
};
|
|
505
515
|
return;
|
|
506
516
|
}
|
|
507
517
|
}
|
|
@@ -7,10 +7,12 @@ import { QueryInput, QuotaThresholdExceededError } from "../../../application/qg
|
|
|
7
7
|
import { type GenerateRequest } from "../common/provider-dispatcher";
|
|
8
8
|
import {
|
|
9
9
|
buildOpenAIResponsesRequest,
|
|
10
|
+
normalizeOpenAIEvent,
|
|
10
11
|
type OpenAINormalizedEvent,
|
|
11
12
|
type OpenAIResponsesOptions,
|
|
12
13
|
} from "./openai-backend-protocol";
|
|
13
14
|
import { ImageGenerationError, OpenAIDispatcher } from "./openai-dispatcher";
|
|
15
|
+
import { buildStandaloneImageRequest } from "./openai-image-generation";
|
|
14
16
|
import { type OpenAITransportKind } from "./openai-transport-config";
|
|
15
17
|
|
|
16
18
|
const credentials = {
|
|
@@ -116,7 +118,7 @@ describe("OpenAIDispatcher direct runtime", () => {
|
|
|
116
118
|
it("applies thinking:false to the automatic image text driver", async () => {
|
|
117
119
|
const run = vi.fn((options: OpenAIResponsesOptions) => {
|
|
118
120
|
expect(options.reasoning).toEqual({ effort: "none" });
|
|
119
|
-
expect(options.imageGeneration).
|
|
121
|
+
expect(options.imageGeneration).toBeUndefined();
|
|
120
122
|
return events({ type: "text-delta", text: "hello" }, { type: "completed", responseId: "r" });
|
|
121
123
|
});
|
|
122
124
|
const d = dispatcher(run);
|
|
@@ -144,6 +146,18 @@ describe("OpenAIDispatcher direct runtime", () => {
|
|
|
144
146
|
}
|
|
145
147
|
});
|
|
146
148
|
|
|
149
|
+
it.each(["auto", "1152x2560"])("forwards %s size hints and preserves hosted observations", async (size) => {
|
|
150
|
+
const args = QueryInput.parse({ prompt: "draw in 9:20", imageGeneration: true, imageGenerationOptions: { size } });
|
|
151
|
+
const generation = { route: "codex-responses" as const, size: "841x1870", quality: "low" };
|
|
152
|
+
const d = dispatcher((options) => {
|
|
153
|
+
expect(buildOpenAIResponsesRequest(options).tools).toContainEqual({ type: "image_generation", size });
|
|
154
|
+
return events({ type: "image", base64: "data", mimeType: "image/png", generation }, { type: "completed", responseId: "r" });
|
|
155
|
+
});
|
|
156
|
+
await d.onTokenAdded(1, "one", credentials);
|
|
157
|
+
const result = await d.generate(request({ imageGeneration: args.imageGeneration, imageGenerationOptions: args.imageGenerationOptions }));
|
|
158
|
+
expect(result.images).toMatchObject([{ generation }]);
|
|
159
|
+
});
|
|
160
|
+
|
|
147
161
|
beforeEach(() => { refreshMock.mockReset(); });
|
|
148
162
|
|
|
149
163
|
it("refreshes a quota 401 once, shares the recovery, and uses the new credentials", async () => {
|
|
@@ -394,31 +408,116 @@ describe("OpenAIDispatcher direct runtime", () => {
|
|
|
394
408
|
));
|
|
395
409
|
const d = dispatcher(run);
|
|
396
410
|
await d.onTokenAdded(1, "one", credentials);
|
|
397
|
-
const result = await d.generate(request({ imageGeneration:
|
|
411
|
+
const result = await d.generate(request({ imageGeneration: true }));
|
|
398
412
|
expect(result.images).toMatchObject([{ data: "first", mediaType: "image/png" }, { data: "second", mediaType: "image/webp" }]);
|
|
399
413
|
expect(run).toHaveBeenCalledTimes(1);
|
|
400
414
|
});
|
|
401
415
|
|
|
402
|
-
it("
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
+
it.each([true, false])("preserves aggregate hosted image usage separately from driver usage (driver usage: %s)", async (withDriverUsage) => {
|
|
417
|
+
const imageUsage = {
|
|
418
|
+
input_tokens: 2260,
|
|
419
|
+
input_tokens_details: { image_tokens: 2012, text_tokens: 248 },
|
|
420
|
+
output_tokens: 1415,
|
|
421
|
+
output_tokens_details: { image_tokens: 1415, text_tokens: 0 },
|
|
422
|
+
total_tokens: 3675,
|
|
423
|
+
};
|
|
424
|
+
const rawEvents = [
|
|
425
|
+
...["first", "second"].map((id) => ({ type: "response.output_item.done", item: {
|
|
426
|
+
id, type: "image_generation_call", result: "UklGRgAAAABXRUJQ",
|
|
427
|
+
size: "1037x1516", quality: "medium", background: "opaque", output_format: "webp",
|
|
428
|
+
} })),
|
|
429
|
+
{ type: "response.completed", response: {
|
|
430
|
+
id: "r", model: "gpt-6-luna",
|
|
431
|
+
...(withDriverUsage ? { usage: { input_tokens: 10, output_tokens: 3, total_tokens: 13 } } : {}),
|
|
432
|
+
tool_usage: { image_gen: imageUsage },
|
|
433
|
+
tools: [{ type: "image_generation", model: "gpt-image-2-codex", quality: "auto", size: "auto", background: "auto", output_format: "webp", n: 1 }],
|
|
434
|
+
} },
|
|
435
|
+
];
|
|
436
|
+
const d = dispatcher((options) => {
|
|
437
|
+
expect(buildOpenAIResponsesRequest(options).tools).toEqual([{
|
|
438
|
+
type: "image_generation", quality: "high", size: "1024x1536", output_format: "webp",
|
|
439
|
+
}]);
|
|
440
|
+
return events(...rawEvents.map(normalizeOpenAIEvent).filter((event): event is OpenAINormalizedEvent => event !== undefined));
|
|
441
|
+
});
|
|
442
|
+
await d.onTokenAdded(1, "one", credentials);
|
|
443
|
+
const result = await d.generate(request({ model: "gpt-6-luna", imageGeneration: true,
|
|
444
|
+
imageGenerationOptions: { quality: "high", size: "1024x1536", outputFormat: "webp" },
|
|
445
|
+
}));
|
|
446
|
+
expect(result.usage).toMatchObject({
|
|
447
|
+
inputTokens: withDriverUsage ? 10 : 0,
|
|
448
|
+
outputTokens: withDriverUsage ? 3 : 0,
|
|
449
|
+
totalTokens: withDriverUsage ? 13 : 0,
|
|
450
|
+
});
|
|
451
|
+
const generation = { route: "codex-responses", model: "gpt-image-2-codex", size: "1037x1516", quality: "medium", background: "opaque" };
|
|
452
|
+
expect(result.images?.map((image) => image.generation)).toEqual([
|
|
453
|
+
{ ...generation, usage: imageUsage }, generation,
|
|
454
|
+
]);
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
it("registers only the internal image function and allows text-only replies", async () => {
|
|
458
|
+
const run = vi.fn((options: OpenAIResponsesOptions) => {
|
|
459
|
+
const body = buildOpenAIResponsesRequest(options);
|
|
460
|
+
expect(options.imageGeneration).toBeUndefined();
|
|
461
|
+
expect(body.parallel_tool_calls).toBe(false);
|
|
462
|
+
expect(body.instructions).toContain("self-contained prompt");
|
|
463
|
+
expect(body.tools).toEqual([expect.objectContaining({
|
|
464
|
+
type: "function", name: "generate_image", strict: false,
|
|
465
|
+
parameters: {
|
|
466
|
+
type: "object", additionalProperties: false,
|
|
467
|
+
required: ["prompt", "transparent_background", "num_last_images_to_include"],
|
|
468
|
+
properties: {
|
|
469
|
+
prompt: expect.objectContaining({ type: "string", minLength: 1 }),
|
|
470
|
+
transparent_background: { type: "boolean" },
|
|
471
|
+
num_last_images_to_include: { type: "integer", minimum: 0, maximum: 5 },
|
|
472
|
+
},
|
|
473
|
+
},
|
|
474
|
+
})]);
|
|
475
|
+
return events({ type: "text-delta", text: "hello" }, { type: "completed", responseId: "r" });
|
|
476
|
+
});
|
|
477
|
+
const d = dispatcher(run);
|
|
478
|
+
await d.onTokenAdded(1, "one", credentials);
|
|
479
|
+
const result = await d.generate(request({ imageGeneration: "auto" }));
|
|
480
|
+
expect(run).toHaveBeenCalledOnce();
|
|
481
|
+
expect(result.text).toBe("hello");
|
|
482
|
+
expect(result.images).toBeUndefined();
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
it.each(["zero-usage", "model-config"])("does not treat completed %s metadata as hosted image execution", async (metadata) => {
|
|
486
|
+
const completed = normalizeOpenAIEvent({
|
|
487
|
+
type: "response.completed",
|
|
488
|
+
response: {
|
|
489
|
+
id: "text-response", model: "gpt-6-luna",
|
|
490
|
+
usage: { input_tokens: 164, output_tokens: 9, total_tokens: 173 },
|
|
491
|
+
tools: metadata === "model-config"
|
|
492
|
+
? [{ type: "image_generation", model: "gpt-image-2-codex" }]
|
|
493
|
+
: [{ type: "function", name: "generate_image" }],
|
|
494
|
+
...(metadata === "zero-usage" ? { tool_usage: { image_gen: {
|
|
495
|
+
input_tokens: 0, output_tokens: 0, total_tokens: 0,
|
|
496
|
+
input_tokens_details: { text_tokens: 0, image_tokens: 0 },
|
|
497
|
+
output_tokens_details: { image_tokens: 0, text_tokens: 0 },
|
|
498
|
+
} } } : {}),
|
|
499
|
+
output: [{ type: "message" }],
|
|
500
|
+
},
|
|
501
|
+
});
|
|
502
|
+
expect(completed).toMatchObject({ type: "completed", imageGeneration: { route: "codex-responses" } });
|
|
503
|
+
const run = vi.fn(() => events({ type: "text-delta", text: "hello" }, completed!));
|
|
504
|
+
const d = dispatcher(run);
|
|
505
|
+
await d.onTokenAdded(1, "one", credentials);
|
|
506
|
+
const result = await d.generate(request({ imageGeneration: "auto" }));
|
|
507
|
+
expect(result).toMatchObject({ text: "hello", model: "gpt-6-luna", usage: { inputTokens: 164, outputTokens: 9, totalTokens: 173 } });
|
|
508
|
+
expect(result.images).toBeUndefined();
|
|
509
|
+
expect(run).toHaveBeenCalledOnce();
|
|
416
510
|
});
|
|
417
511
|
|
|
418
|
-
it("runs
|
|
512
|
+
it.each([false, true])("runs a standalone image call (transparent: %s) and preserves driver and image usage", async (transparent) => {
|
|
419
513
|
const calls: OpenAIResponsesOptions[] = [];
|
|
420
514
|
const signals: Array<AbortSignal | undefined> = [];
|
|
421
|
-
const item = { type: "function_call", call_id: "call", name: "
|
|
515
|
+
const item = { type: "function_call", call_id: "call", name: "generate_image", arguments: JSON.stringify({ prompt: "a complete new image prompt", transparent_background: transparent, num_last_images_to_include: 0 }) };
|
|
516
|
+
const generation = {
|
|
517
|
+
route: "codex-images" as const, model: "gpt-image-2", size: "1024x1024", quality: "medium",
|
|
518
|
+
background: transparent ? "transparent" : "opaque",
|
|
519
|
+
usage: { input_tokens: 20, input_tokens_details: { text_tokens: 20, image_tokens: 0 }, output_tokens: 100, total_tokens: 120 },
|
|
520
|
+
};
|
|
422
521
|
const d = dispatcher((options, signal) => {
|
|
423
522
|
calls.push(options); signals.push(signal);
|
|
424
523
|
if (calls.length === 1) return events(
|
|
@@ -427,29 +526,77 @@ describe("OpenAIDispatcher direct runtime", () => {
|
|
|
427
526
|
{ type: "output-item", item, completed: true },
|
|
428
527
|
{ type: "completed", responseId: "driver", model: "actual-driver", usage: { inputTokens: 10, cachedInputTokens: 2, outputTokens: 3, reasoningTokens: 1, totalTokens: 13 } },
|
|
429
528
|
);
|
|
430
|
-
return events({ type: "image", base64: "png", mimeType: "image/png", generation
|
|
529
|
+
return events({ type: "image", base64: "png", mimeType: "image/png", generation }, { type: "completed", responseId: "image", model: "gpt-image-2", usage: { inputTokens: 999, outputTokens: 999, cachedInputTokens: 0, reasoningTokens: 0, totalTokens: 1998 } });
|
|
431
530
|
});
|
|
432
531
|
await d.onTokenAdded(1, "one", credentials);
|
|
433
532
|
const history = [{ type: "message", role: "user", content: [{ type: "input_image", image_url: "https://example.com/ref.png" }] }];
|
|
434
|
-
const result = await d.generate(request({ imageGeneration: "auto", systemPrompt: "system", coldHistory: history, imageGenerationOptions: {
|
|
533
|
+
const result = await d.generate(request({ imageGeneration: "auto", systemPrompt: "system", coldHistory: history, imageGenerationOptions: { quality: "high", size: "1024x1536" } }));
|
|
435
534
|
expect(calls).toHaveLength(2);
|
|
436
|
-
expect(calls[
|
|
437
|
-
expect(calls[
|
|
535
|
+
expect(calls[0]!.instructions).toContain("system");
|
|
536
|
+
expect(calls[0]!.history[0]).toEqual(history[0]);
|
|
537
|
+
expect(calls[1]).toEqual({
|
|
538
|
+
model: "gpt-image-2", imageGeneration: { background: transparent ? "transparent" : "opaque", output_format: "png", quality: "high", size: "1024x1536" },
|
|
539
|
+
history: [{ type: "message", role: "user", content: [{ type: "input_text", text: "a complete new image prompt" }] }],
|
|
540
|
+
});
|
|
541
|
+
expect(buildStandaloneImageRequest(calls[1]!).url).toMatch(/\/images\/generations$/);
|
|
438
542
|
expect(calls[1]!.tools).toBeUndefined();
|
|
439
543
|
expect(calls[1]!.outputSchema).toBeUndefined();
|
|
440
544
|
expect(signals[0]).toBe(signals[1]);
|
|
441
|
-
expect(result).toMatchObject({ model: "actual-driver", usage: { inputTokens: 10, outputTokens: 3, totalTokens: 13 }, images: [{ generation
|
|
545
|
+
expect(result).toMatchObject({ model: "actual-driver", usage: { inputTokens: 10, outputTokens: 3, totalTokens: 13, cachedInputTokens: 2, reasoningOutputTokens: 1 }, images: [{ generation }] });
|
|
546
|
+
expect(d.clients.size).toBe(1);
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
it("edits with only the selected recent images in chronological order despite longer image history", async () => {
|
|
550
|
+
const calls: OpenAIResponsesOptions[] = [];
|
|
551
|
+
const history = [
|
|
552
|
+
...Array.from({ length: 6 }, (_, i) => ({ type: "message", role: "user", content: [{ type: "input_image", image_url: `https://example.com/${i}.png` }] })),
|
|
553
|
+
{ type: "image_generation_call", result: "UklGRgAAAABXRUJQ", output_format: "webp" },
|
|
554
|
+
{ type: "message", role: "user", content: [{ type: "image_url", image_url: { url: "https://example.com/last.png" } }] },
|
|
555
|
+
];
|
|
556
|
+
const d = dispatcher((options) => {
|
|
557
|
+
calls.push(options);
|
|
558
|
+
if (calls.length === 1) return events(
|
|
559
|
+
{ type: "output-item", completed: true, item: { type: "function_call", call_id: "edit", name: "generate_image", arguments: JSON.stringify({ prompt: "combine these references", transparent_background: false, num_last_images_to_include: 3 }) } },
|
|
560
|
+
{ type: "completed", responseId: "driver" },
|
|
561
|
+
);
|
|
562
|
+
return events({ type: "image", base64: "png", mimeType: "image/png" }, { type: "completed", responseId: "image" });
|
|
563
|
+
});
|
|
564
|
+
await d.onTokenAdded(1, "one", credentials);
|
|
565
|
+
await d.generate(request({ imageGeneration: "auto", coldHistory: history, coldInput: [{ type: "image", url: "https://example.com/current.png" }] }));
|
|
566
|
+
expect(calls[0]!.history).toHaveLength(history.length + 1);
|
|
567
|
+
const direct = buildStandaloneImageRequest(calls[1]!);
|
|
568
|
+
expect(direct.url).toMatch(/\/images\/edits$/);
|
|
569
|
+
expect(direct.body.prompt).toBe("user:\ncombine these references");
|
|
570
|
+
expect(direct.body.images).toEqual([
|
|
571
|
+
{ image_url: "data:image/webp;base64,UklGRgAAAABXRUJQ" },
|
|
572
|
+
{ image_url: "https://example.com/last.png" },
|
|
573
|
+
{ image_url: "https://example.com/current.png" },
|
|
574
|
+
]);
|
|
575
|
+
expect(calls).toHaveLength(2);
|
|
442
576
|
});
|
|
443
577
|
|
|
444
578
|
it("rejects incomplete, invalid, multiple and mixed automatic image calls before standalone execution", async () => {
|
|
445
|
-
const
|
|
579
|
+
const args = { prompt: "remove background", transparent_background: true, num_last_images_to_include: 0 };
|
|
580
|
+
const item = { type: "function_call", call_id: "call", name: "generate_image", arguments: JSON.stringify(args) };
|
|
446
581
|
const done = { type: "output-item" as const, item, completed: true };
|
|
447
582
|
const badEvents: OpenAINormalizedEvent[][] = [
|
|
448
583
|
[{ ...done, completed: false }],
|
|
449
|
-
[
|
|
584
|
+
...[
|
|
585
|
+
"{", "[]", "null", JSON.stringify({ prompt: "x" }),
|
|
586
|
+
JSON.stringify({ ...args, extra: true }),
|
|
587
|
+
JSON.stringify({ ...args, prompt: " " }),
|
|
588
|
+
JSON.stringify({ ...args, transparent_background: "true" }),
|
|
589
|
+
...[-1, 0.5, 6, "1"].map((count) => JSON.stringify({ ...args, num_last_images_to_include: count })),
|
|
590
|
+
// Valid count, but this conversation has no images.
|
|
591
|
+
JSON.stringify({ ...args, num_last_images_to_include: 1 }),
|
|
592
|
+
].map((arguments_) => [{ ...done, item: { ...item, arguments: arguments_ } }]),
|
|
450
593
|
[done, { ...done, item: { ...item, call_id: "second" } }],
|
|
594
|
+
[done, { ...done, item: { ...item, arguments: JSON.stringify({ ...args, transparent_background: false }) } }],
|
|
595
|
+
[{ ...done, item: { ...item, call_id: "" } }],
|
|
596
|
+
[{ ...done, item: { ...item, name: "unexpected" } }],
|
|
451
597
|
[done, { type: "image", base64: "png", mimeType: "image/png" }],
|
|
452
598
|
[{ type: "output-item", item: { type: "image_generation_call" }, completed: false }],
|
|
599
|
+
[{ type: "completed", responseId: "native", imageGeneration: { route: "codex-responses", usage: { input_tokens: 1, output_tokens: 0, total_tokens: 1 } } }],
|
|
453
600
|
];
|
|
454
601
|
for (const stream of badEvents) {
|
|
455
602
|
const run = vi.fn(() => events(...stream, { type: "completed", responseId: "driver" }));
|
|
@@ -460,6 +607,25 @@ describe("OpenAIDispatcher direct runtime", () => {
|
|
|
460
607
|
}
|
|
461
608
|
});
|
|
462
609
|
|
|
610
|
+
it("requires driver completion before running its image call", async () => {
|
|
611
|
+
const run = vi.fn(() => events({ type: "output-item", completed: true, item: {
|
|
612
|
+
type: "function_call", call_id: "call", name: "generate_image",
|
|
613
|
+
arguments: JSON.stringify({ prompt: "draw", transparent_background: false, num_last_images_to_include: 0 }),
|
|
614
|
+
} }));
|
|
615
|
+
const d = dispatcher(run);
|
|
616
|
+
await d.onTokenAdded(1, "one", credentials);
|
|
617
|
+
await expect(d.generate(request({ imageGeneration: "auto" }))).rejects.toMatchObject({ kind: "incomplete" });
|
|
618
|
+
expect(run).toHaveBeenCalledOnce();
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
it.each(["jpeg", "webp"] as const)("rejects automatic %s output before driver transport", async (outputFormat) => {
|
|
622
|
+
const run = vi.fn(() => events());
|
|
623
|
+
const d = dispatcher(run);
|
|
624
|
+
await d.onTokenAdded(1, "one", credentials);
|
|
625
|
+
await expect(d.generate(request({ imageGeneration: "auto", imageGenerationOptions: { outputFormat } }))).rejects.toMatchObject({ kind: "gate", message: expect.stringContaining("PNG") });
|
|
626
|
+
expect(run).not.toHaveBeenCalled();
|
|
627
|
+
});
|
|
628
|
+
|
|
463
629
|
it("rejects structured output in auto mode before transport", async () => {
|
|
464
630
|
const run = vi.fn(() => events());
|
|
465
631
|
const d = dispatcher(run);
|
|
@@ -468,38 +634,75 @@ describe("OpenAIDispatcher direct runtime", () => {
|
|
|
468
634
|
expect(run).not.toHaveBeenCalled();
|
|
469
635
|
});
|
|
470
636
|
|
|
471
|
-
it.each(["missing-image", "missing-completion", "cancel"])("
|
|
637
|
+
it.each(["missing-image", "missing-completion", "cancel", "error", "network", "references"])("keeps completed driver accounting after standalone %s", async (failure) => {
|
|
472
638
|
const controller = new AbortController();
|
|
639
|
+
const networkError = new TypeError("fetch failed", { cause: new Error("connection reset") });
|
|
473
640
|
let count = 0;
|
|
474
641
|
const d = dispatcher(async function* (_options, signal) {
|
|
475
642
|
count++;
|
|
476
643
|
if (count === 1) {
|
|
477
|
-
yield { type: "
|
|
478
|
-
yield { type: "completed",
|
|
644
|
+
yield { type: "text-delta", text: "I will draw it." };
|
|
645
|
+
yield { type: "output-item", completed: true, item: { type: "function_call", call_id: "call", name: "generate_image", arguments: JSON.stringify({ prompt: "transparent", transparent_background: true, num_last_images_to_include: failure === "references" ? 1 : 0 }) } };
|
|
646
|
+
yield { type: "completed", responseId: "driver", model: "gpt-6-luna", usage: { inputTokens: 100, outputTokens: 20, cachedInputTokens: 10, reasoningTokens: 5, totalTokens: 120 } };
|
|
479
647
|
} else {
|
|
480
648
|
if (failure === "cancel") { controller.abort(); expect(signal?.aborted).toBe(true); throw signal!.reason; }
|
|
649
|
+
if (failure === "network") throw networkError;
|
|
650
|
+
if (failure === "error") { yield { type: "error", error: new Error("image failed") }; return; }
|
|
481
651
|
if (failure === "missing-image") yield { type: "completed", responseId: "image" };
|
|
482
652
|
else yield { type: "image", base64: "png", mimeType: "image/png" };
|
|
483
653
|
}
|
|
484
654
|
});
|
|
485
655
|
await d.onTokenAdded(1, "one", credentials);
|
|
486
|
-
await
|
|
487
|
-
expect(
|
|
656
|
+
const error = await d.generate(request({ imageGeneration: "auto", abortSignal: controller.signal })).catch((error) => error);
|
|
657
|
+
expect(error).toMatchObject({
|
|
658
|
+
kind: "incomplete",
|
|
659
|
+
name: failure === "cancel" ? "AbortError" : failure === "network" ? "TypeError" : "ImageGenerationError",
|
|
660
|
+
completedDriver: {
|
|
661
|
+
text: "I will draw it.", tokenName: "one", model: "gpt-6-luna", requestedModel: "gpt-test",
|
|
662
|
+
usage: { inputTokens: 100, outputTokens: 20, cachedInputTokens: 10, reasoningOutputTokens: 5, totalTokens: 120 },
|
|
663
|
+
durationMs: expect.any(Number), ttftMs: expect.any(Number),
|
|
664
|
+
},
|
|
665
|
+
});
|
|
666
|
+
expect(error.completedDriver.images).toBeUndefined();
|
|
667
|
+
if (failure === "network") {
|
|
668
|
+
expect(error.message).toBe(networkError.message);
|
|
669
|
+
expect(error.cause).toBe(networkError);
|
|
670
|
+
expect(error.cause.cause.message).toBe("connection reset");
|
|
671
|
+
}
|
|
672
|
+
expect(count).toBe(failure === "references" ? 1 : 2);
|
|
488
673
|
});
|
|
489
674
|
|
|
490
675
|
it("does not start standalone generation after cancellation", async () => {
|
|
491
676
|
const controller = new AbortController();
|
|
492
677
|
const run = vi.fn(async function* () {
|
|
493
|
-
yield { type: "output-item" as const, completed: true, item: { type: "function_call", call_id: "call", name: "
|
|
678
|
+
yield { type: "output-item" as const, completed: true, item: { type: "function_call", call_id: "call", name: "generate_image", arguments: '{"prompt":"transparent","transparent_background":true,"num_last_images_to_include":0}' } };
|
|
494
679
|
yield { type: "completed" as const, responseId: "driver" };
|
|
495
680
|
controller.abort();
|
|
496
681
|
});
|
|
497
682
|
const d = dispatcher(run);
|
|
498
683
|
await d.onTokenAdded(1, "one", credentials);
|
|
499
|
-
await expect(d.generate(request({ imageGeneration: "auto", abortSignal: controller.signal }))).rejects.toMatchObject({ name: "AbortError" });
|
|
684
|
+
await expect(d.generate(request({ imageGeneration: "auto", abortSignal: controller.signal }))).rejects.toMatchObject({ name: "AbortError", completedDriver: { tokenName: "one" } });
|
|
500
685
|
expect(run).toHaveBeenCalledTimes(1);
|
|
501
686
|
});
|
|
502
687
|
|
|
688
|
+
it.each(["incomplete-driver", "direct-image", "plain-text"])("does not invent completed driver accounting for %s", async (path) => {
|
|
689
|
+
const failure = new Error("generation failed");
|
|
690
|
+
const d = dispatcher(async function* () {
|
|
691
|
+
if (path === "incomplete-driver") yield { type: "output-item", completed: true, item: {
|
|
692
|
+
type: "function_call", call_id: "call", name: "generate_image", arguments: '{"prompt":"draw","transparent_background":false,"num_last_images_to_include":0}',
|
|
693
|
+
} };
|
|
694
|
+
else yield { type: "completed", responseId: "response" };
|
|
695
|
+
throw failure;
|
|
696
|
+
});
|
|
697
|
+
await d.onTokenAdded(1, "one", credentials);
|
|
698
|
+
const error = await d.generate(request({
|
|
699
|
+
imageGeneration: path === "incomplete-driver" ? "auto" : path === "direct-image" ? true : undefined,
|
|
700
|
+
model: path === "direct-image" ? "gpt-image-2" : "gpt-6-luna",
|
|
701
|
+
})).catch((error) => error);
|
|
702
|
+
expect(error).toBe(failure);
|
|
703
|
+
expect(error.completedDriver).toBeUndefined();
|
|
704
|
+
});
|
|
705
|
+
|
|
503
706
|
it("maps images and preserves image failure classifications", async () => {
|
|
504
707
|
const success = dispatcher(() =>
|
|
505
708
|
events(
|