@cartanova/qgrid-cli 2.10.5 → 2.10.7
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 +26 -7
- package/bundle/dist/application/qgrid/qgrid.dispatcher.js +12 -7
- 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 +5 -3
- package/bundle/dist/application/sonamu.generated.js +4 -1
- package/bundle/dist/application/sonamu.generated.sso.js +3 -1
- package/bundle/dist/i18n/sd.generated.js +4 -3
- package/bundle/dist/migrations/20261016155350_alter_request_logs_add1_alter1.js +15 -0
- 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/common/model-cost.js +14 -3
- 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 +105 -9
- package/bundle/src/application/qgrid/qgrid-run-lifecycle.ts +47 -5
- package/bundle/src/application/qgrid/qgrid.dispatcher.test.ts +160 -2
- package/bundle/src/application/qgrid/qgrid.dispatcher.ts +40 -18
- package/bundle/src/application/qgrid/qgrid.frame.test.ts +74 -0
- package/bundle/src/application/qgrid/qgrid.frame.ts +39 -4
- package/bundle/src/application/request-log/request-log.entity.json +11 -2
- package/bundle/src/application/request-log/request-log.model.ts +4 -0
- package/bundle/src/application/sonamu.generated.http +2 -2
- package/bundle/src/application/sonamu.generated.sso.ts +2 -0
- package/bundle/src/application/sonamu.generated.ts +4 -0
- package/bundle/src/i18n/sd.generated.ts +3 -2
- package/bundle/src/migrations/20261016155350_alter_request_logs_add1_alter1.ts +15 -0
- package/bundle/src/utils/providers/common/model-cost.test.ts +28 -0
- package/bundle/src/utils/providers/common/model-cost.ts +19 -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-Bw2s88Ot.css +1 -0
- package/bundle/web-dist/client/assets/{index-Dxw7DiXF.js → index-CwqqPF4F.js} +4 -4
- package/bundle/web-dist/client/assets/logs-CZgl1RSu.js +1 -0
- package/bundle/web-dist/client/assets/{monit-E8eGW1Sj.js → monit-TarvUWiE.js} +1 -1
- package/bundle/web-dist/client/assets/request-model-CojtinqK.js +1 -0
- package/bundle/web-dist/client/assets/{routes-7spLfIGZ.js → routes-BXR6OLah.js} +2 -2
- package/bundle/web-dist/client/assets/{services.generated-jnPHYMER.js → services.generated-BsieU37H.js} +1 -1
- package/bundle/web-dist/client/assets/{settings-BdosZFNC.js → settings-B1y5MNSU.js} +1 -1
- package/bundle/web-dist/client/assets/show-CG-BAdhS.js +2 -0
- package/bundle/web-dist/client/assets/{tokens-CBdHb2_U.js → tokens-D9tAWBEW.js} +1 -1
- package/bundle/web-dist/client/assets/{use-oauth-login-flow-Cyn3YPqW.js → use-oauth-login-flow-DxdOhz4a.js} +1 -1
- package/bundle/web-dist/client/assets/{use-update-token-mutation-DQCwCYfR.js → use-update-token-mutation-DAq0F8p-.js} +1 -1
- package/bundle/web-dist/client/index.html +3 -3
- 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-Dtd0NR4r.js} +2 -2
- package/bundle/web-dist/server/assets/{logs-CaaWO9-Z.js → logs-l91avYif.js} +33 -21
- package/bundle/web-dist/server/assets/{monit-CBil92fJ.js → monit-DvzribSE.js} +4 -4
- package/bundle/web-dist/server/assets/{plus-Bvdj8Pwx.js → plus-KZrECewT.js} +1 -1
- package/bundle/web-dist/server/assets/{react-dom-CyuShs9K.js → react-dom-B4a6-uuR.js} +1 -1
- package/bundle/web-dist/server/assets/{refresh-cw-3QAiSWBd.js → refresh-cw-BND93JrF.js} +1 -1
- package/bundle/web-dist/server/assets/{request-model-CnCeaNLE.js → request-model-CbqfnQOJ.js} +19 -1
- package/bundle/web-dist/server/assets/{routes-DtVf9yqo.js → routes-BAPWTizT.js} +6 -7
- package/bundle/web-dist/server/assets/{services.generated-FPwsG9p3.js → services.generated-CEbGjsI0.js} +3 -2
- package/bundle/web-dist/server/assets/{settings-DeHNwoMv.js → settings-BZR5HSAL.js} +3 -3
- package/bundle/web-dist/server/assets/{show-8NNdRxhM.js → show-CxVZTAdv.js} +17 -21
- package/bundle/web-dist/server/assets/{tokens-odgS2-9q.js → tokens-D1hb4eKk.js} +4 -4
- package/bundle/web-dist/server/assets/{use-oauth-login-flow-DTQKdYBO.js → use-oauth-login-flow-Cvdy0zF8.js} +1 -1
- package/bundle/web-dist/server/assets/{use-update-token-mutation-xvXbPrCI.js → use-update-token-mutation-CfYmk_4_.js} +1 -1
- package/bundle/web-dist/server/entry-server.generated.js +13 -10
- package/package.json +1 -1
- package/skills/qgrid/SKILL.md +9 -8
- package/skills/qgrid/references/ai-sdk-provider-contract.md +27 -17
- package/skills/qgrid/references/decision-rationale.md +8 -8
- package/skills/qgrid/references/openai-codex-runtime.md +19 -11
- package/skills/qgrid/references/prompt-cache-and-usage.md +3 -0
- package/skills/qgrid/references/request-log-run-lifecycle.md +14 -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/index-CvG4UB9v.css +0 -1
- package/bundle/web-dist/client/assets/logs-3n_U4qnI.js +0 -1
- package/bundle/web-dist/client/assets/request-model-D4CZPmnG.js +0 -1
- package/bundle/web-dist/client/assets/show-b2PFNI3D.js +0 -2
|
@@ -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);
|
|
@@ -406,31 +408,116 @@ describe("OpenAIDispatcher direct runtime", () => {
|
|
|
406
408
|
));
|
|
407
409
|
const d = dispatcher(run);
|
|
408
410
|
await d.onTokenAdded(1, "one", credentials);
|
|
409
|
-
const result = await d.generate(request({ imageGeneration:
|
|
411
|
+
const result = await d.generate(request({ imageGeneration: true }));
|
|
410
412
|
expect(result.images).toMatchObject([{ data: "first", mediaType: "image/png" }, { data: "second", mediaType: "image/webp" }]);
|
|
411
413
|
expect(run).toHaveBeenCalledTimes(1);
|
|
412
414
|
});
|
|
413
415
|
|
|
414
|
-
it("
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
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();
|
|
428
510
|
});
|
|
429
511
|
|
|
430
|
-
it("runs
|
|
512
|
+
it.each([false, true])("runs a standalone image call (transparent: %s) and preserves driver and image usage", async (transparent) => {
|
|
431
513
|
const calls: OpenAIResponsesOptions[] = [];
|
|
432
514
|
const signals: Array<AbortSignal | undefined> = [];
|
|
433
|
-
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
|
+
};
|
|
434
521
|
const d = dispatcher((options, signal) => {
|
|
435
522
|
calls.push(options); signals.push(signal);
|
|
436
523
|
if (calls.length === 1) return events(
|
|
@@ -439,29 +526,77 @@ describe("OpenAIDispatcher direct runtime", () => {
|
|
|
439
526
|
{ type: "output-item", item, completed: true },
|
|
440
527
|
{ type: "completed", responseId: "driver", model: "actual-driver", usage: { inputTokens: 10, cachedInputTokens: 2, outputTokens: 3, reasoningTokens: 1, totalTokens: 13 } },
|
|
441
528
|
);
|
|
442
|
-
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 } });
|
|
443
530
|
});
|
|
444
531
|
await d.onTokenAdded(1, "one", credentials);
|
|
445
532
|
const history = [{ type: "message", role: "user", content: [{ type: "input_image", image_url: "https://example.com/ref.png" }] }];
|
|
446
|
-
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" } }));
|
|
447
534
|
expect(calls).toHaveLength(2);
|
|
448
|
-
expect(calls[
|
|
449
|
-
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$/);
|
|
450
542
|
expect(calls[1]!.tools).toBeUndefined();
|
|
451
543
|
expect(calls[1]!.outputSchema).toBeUndefined();
|
|
452
544
|
expect(signals[0]).toBe(signals[1]);
|
|
453
|
-
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);
|
|
454
576
|
});
|
|
455
577
|
|
|
456
578
|
it("rejects incomplete, invalid, multiple and mixed automatic image calls before standalone execution", async () => {
|
|
457
|
-
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) };
|
|
458
581
|
const done = { type: "output-item" as const, item, completed: true };
|
|
459
582
|
const badEvents: OpenAINormalizedEvent[][] = [
|
|
460
583
|
[{ ...done, completed: false }],
|
|
461
|
-
[
|
|
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_ } }]),
|
|
462
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" } }],
|
|
463
597
|
[done, { type: "image", base64: "png", mimeType: "image/png" }],
|
|
464
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 } } }],
|
|
465
600
|
];
|
|
466
601
|
for (const stream of badEvents) {
|
|
467
602
|
const run = vi.fn(() => events(...stream, { type: "completed", responseId: "driver" }));
|
|
@@ -472,6 +607,25 @@ describe("OpenAIDispatcher direct runtime", () => {
|
|
|
472
607
|
}
|
|
473
608
|
});
|
|
474
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
|
+
|
|
475
629
|
it("rejects structured output in auto mode before transport", async () => {
|
|
476
630
|
const run = vi.fn(() => events());
|
|
477
631
|
const d = dispatcher(run);
|
|
@@ -480,38 +634,75 @@ describe("OpenAIDispatcher direct runtime", () => {
|
|
|
480
634
|
expect(run).not.toHaveBeenCalled();
|
|
481
635
|
});
|
|
482
636
|
|
|
483
|
-
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) => {
|
|
484
638
|
const controller = new AbortController();
|
|
639
|
+
const networkError = new TypeError("fetch failed", { cause: new Error("connection reset") });
|
|
485
640
|
let count = 0;
|
|
486
641
|
const d = dispatcher(async function* (_options, signal) {
|
|
487
642
|
count++;
|
|
488
643
|
if (count === 1) {
|
|
489
|
-
yield { type: "
|
|
490
|
-
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 } };
|
|
491
647
|
} else {
|
|
492
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; }
|
|
493
651
|
if (failure === "missing-image") yield { type: "completed", responseId: "image" };
|
|
494
652
|
else yield { type: "image", base64: "png", mimeType: "image/png" };
|
|
495
653
|
}
|
|
496
654
|
});
|
|
497
655
|
await d.onTokenAdded(1, "one", credentials);
|
|
498
|
-
await
|
|
499
|
-
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);
|
|
500
673
|
});
|
|
501
674
|
|
|
502
675
|
it("does not start standalone generation after cancellation", async () => {
|
|
503
676
|
const controller = new AbortController();
|
|
504
677
|
const run = vi.fn(async function* () {
|
|
505
|
-
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}' } };
|
|
506
679
|
yield { type: "completed" as const, responseId: "driver" };
|
|
507
680
|
controller.abort();
|
|
508
681
|
});
|
|
509
682
|
const d = dispatcher(run);
|
|
510
683
|
await d.onTokenAdded(1, "one", credentials);
|
|
511
|
-
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" } });
|
|
512
685
|
expect(run).toHaveBeenCalledTimes(1);
|
|
513
686
|
});
|
|
514
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
|
+
|
|
515
706
|
it("maps images and preserves image failure classifications", async () => {
|
|
516
707
|
const success = dispatcher(() =>
|
|
517
708
|
events(
|