@cartanova/qgrid-cli 2.9.7 → 2.9.9

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.
Files changed (66) hide show
  1. package/bundle/dist/application/qgrid/qgrid-run-lifecycle.js +3 -2
  2. package/bundle/dist/application/qgrid/qgrid.types.js +2 -2
  3. package/bundle/dist/application/request-log/request-log.model.js +2 -1
  4. package/bundle/dist/utils/providers/anthropic/anthropic-constants.js +4 -2
  5. package/bundle/dist/utils/providers/common/model-cost.js +3 -2
  6. package/bundle/dist/utils/providers/openai/openai-backend-protocol.js +3 -2
  7. package/bundle/dist/utils/providers/openai/openai-dispatcher.js +86 -5
  8. package/bundle/src/application/qgrid/qgrid-image-generation.test.ts +7 -1
  9. package/bundle/src/application/qgrid/qgrid-run-lifecycle.test.ts +13 -0
  10. package/bundle/src/application/qgrid/qgrid-run-lifecycle.ts +2 -1
  11. package/bundle/src/application/qgrid/qgrid.frame.test.ts +2 -2
  12. package/bundle/src/application/qgrid/qgrid.types.ts +2 -2
  13. package/bundle/src/application/request-log/request-log.model.ts +2 -0
  14. package/bundle/src/application/sonamu.generated.http +4 -2
  15. package/bundle/src/utils/providers/anthropic/anthropic-constants.test.ts +5 -1
  16. package/bundle/src/utils/providers/anthropic/anthropic-constants.ts +4 -2
  17. package/bundle/src/utils/providers/common/model-cost.test.ts +1 -0
  18. package/bundle/src/utils/providers/common/model-cost.ts +3 -0
  19. package/bundle/src/utils/providers/common/provider-dispatcher.ts +1 -1
  20. package/bundle/src/utils/providers/openai/openai-backend-protocol.test.ts +5 -0
  21. package/bundle/src/utils/providers/openai/openai-backend-protocol.ts +2 -2
  22. package/bundle/src/utils/providers/openai/openai-dispatcher.test.ts +114 -0
  23. package/bundle/src/utils/providers/openai/openai-dispatcher.ts +136 -1
  24. package/bundle/web-dist/client/assets/index-D8VwLsuq.js +106 -0
  25. package/bundle/web-dist/client/assets/index-D_sagzsR.css +1 -0
  26. package/bundle/web-dist/client/assets/logs-_T8KV5ne.js +1 -0
  27. package/bundle/web-dist/client/assets/{monit-7tcWgB20.js → monit-DRCrtO2j.js} +1 -1
  28. package/bundle/web-dist/client/assets/request-model-eygfdRv7.js +1 -0
  29. package/bundle/web-dist/client/assets/{routes-CSVp9aNn.js → routes-5A6mia6C.js} +1 -1
  30. package/bundle/web-dist/client/assets/{services.generated-C6jfEwfK.js → services.generated-Bg2XkD-D.js} +1 -1
  31. package/bundle/web-dist/client/assets/settings-BpP9WVFI.js +1 -0
  32. package/bundle/web-dist/client/assets/show-D5ifZXdy.js +2 -0
  33. package/bundle/web-dist/client/assets/tokens-Cfe6WvLK.js +1 -0
  34. package/bundle/web-dist/client/assets/{use-oauth-login-flow-DTrmdduV.js → use-oauth-login-flow-BTLUGhwh.js} +1 -1
  35. package/bundle/web-dist/client/assets/{use-update-token-mutation-BIvO70-M.js → use-update-token-mutation-Dkv_HG-C.js} +1 -1
  36. package/bundle/web-dist/client/index.html +3 -3
  37. package/bundle/web-dist/server/assets/{lazyRouteComponent-C_k4R5n6.js → lazyRouteComponent-DyizoQKi.js} +2 -2
  38. package/bundle/web-dist/server/assets/{logs-lBOU5eUP.js → logs-DO8zBdBR.js} +12 -5
  39. package/bundle/web-dist/server/assets/{monit-BW2X1D23.js → monit-COH4MckI.js} +2 -2
  40. package/bundle/web-dist/server/assets/{plus-DilmHifT.js → plus-C1D8z9o0.js} +1 -1
  41. package/bundle/web-dist/server/assets/{react-dom-CHRRsKwb.js → react-dom-PfWvArTX.js} +1 -1
  42. package/bundle/web-dist/server/assets/request-model-DXclJfdP.js +13 -0
  43. package/bundle/web-dist/server/assets/{routes-B408iqh1.js → routes-Btja-741.js} +4 -4
  44. package/bundle/web-dist/server/assets/{services.generated-buX3cJzR.js → services.generated-BsKOUSXl.js} +1 -1
  45. package/bundle/web-dist/server/assets/{settings-CFoXQaAg.js → settings-DPcWGvy-.js} +3 -3
  46. package/bundle/web-dist/server/assets/{show-CcZJO6WF.js → show-CMXbb3TJ.js} +10 -4
  47. package/bundle/web-dist/server/assets/{tokens-C32lycTe.js → tokens-DAUmTrgO.js} +15 -11
  48. package/bundle/web-dist/server/assets/{use-oauth-login-flow-DCjxIa80.js → use-oauth-login-flow-GsGwqefs.js} +1 -1
  49. package/bundle/web-dist/server/assets/{use-update-token-mutation-CqK8i8Fa.js → use-update-token-mutation-RujWERvj.js} +1 -1
  50. package/bundle/web-dist/server/entry-server.generated.js +272 -51
  51. package/package.json +1 -1
  52. package/skills/qgrid/SKILL.md +10 -1
  53. package/skills/qgrid/references/anthropic-claude-code-runtime.md +3 -2
  54. package/skills/qgrid/references/decision-rationale.md +11 -2
  55. package/skills/qgrid/references/openai-codex-runtime.md +11 -1
  56. package/skills/qgrid/references/request-log-run-lifecycle.md +3 -1
  57. package/skills/qgrid/references/sonamu-api-web-flow.md +9 -1
  58. package/skills/qgrid/references/token-auth-quota-lifecycle.md +2 -0
  59. package/skills/qgrid/references/verification-and-debugging.md +4 -1
  60. package/bundle/web-dist/client/assets/index-BshX4Gd8.css +0 -1
  61. package/bundle/web-dist/client/assets/index-D4MEKFjK.js +0 -106
  62. package/bundle/web-dist/client/assets/logs-BWhH8t7Y.js +0 -1
  63. package/bundle/web-dist/client/assets/settings-CYgR0YTh.js +0 -1
  64. package/bundle/web-dist/client/assets/show-Cg8n_Mxg.js +0 -2
  65. package/bundle/web-dist/client/assets/tokens-B5yg6bP1.js +0 -1
  66. /package/bundle/web-dist/client/assets/{plus-p4vtg6LN.js → plus-CknMNCP5.js} +0 -0
@@ -8,6 +8,11 @@ import {
8
8
  } from "./openai-backend-protocol";
9
9
 
10
10
  describe("OpenAI Codex backend protocol", () => {
11
+ it("distinguishes partial function items from completed arguments", () => {
12
+ const item = { type: "function_call", call_id: "call", name: "generate_transparent_image", arguments: "{}" };
13
+ expect(normalizeOpenAIEvent({ type: "response.output_item.added", item })).toEqual({ type: "output-item", item, completed: false });
14
+ expect(normalizeOpenAIEvent({ type: "response.output_item.done", item })).toEqual({ type: "output-item", item, completed: true });
15
+ });
11
16
  it("uses returned bytes rather than a mismatched format label", () => {
12
17
  expect(normalizeOpenAIEvent({ type: "response.output_item.done", item: {
13
18
  type: "image_generation_call", result: "UklGRgAAAABXRUJQ", output_format: "png",
@@ -179,7 +179,7 @@ export type OpenAINormalizedEvent =
179
179
  revisedPrompt?: string;
180
180
  generation?: ImageGenerationMetadata;
181
181
  }
182
- | { type: "output-item"; item: OpenAIResponseItem }
182
+ | { type: "output-item"; item: OpenAIResponseItem; completed?: boolean }
183
183
  | { type: "completed"; responseId: string; usage?: OpenAIUsage; model?: string }
184
184
  | { type: "error"; error: OpenAIProtocolError };
185
185
 
@@ -256,7 +256,7 @@ export function normalizeOpenAIEvent(raw: unknown): OpenAINormalizedEvent | unde
256
256
  : {}),
257
257
  };
258
258
  }
259
- return { type: "output-item", item };
259
+ return { type: "output-item", item, completed: type === "response.output_item.done" };
260
260
  }
261
261
  if (type === "response.completed") {
262
262
  const response = (event.response ?? {}) as Record<string, unknown>;
@@ -295,6 +295,120 @@ describe("OpenAIDispatcher direct runtime", () => {
295
295
  expect(complete).toHaveBeenCalledWith(expect.objectContaining({ text: "ab" }));
296
296
  });
297
297
 
298
+ it("returns multiple hosted images with one driver request", async () => {
299
+ const run = vi.fn(() => events(
300
+ { type: "image", id: "first", base64: "first", mimeType: "image/png" },
301
+ { type: "image", id: "second", base64: "second", mimeType: "image/webp" },
302
+ { type: "completed", responseId: "driver" },
303
+ ));
304
+ const d = dispatcher(run);
305
+ await d.onTokenAdded(1, "one", credentials);
306
+ const result = await d.generate(request({ imageGeneration: "auto" }));
307
+ expect(result.images).toMatchObject([{ data: "first", mediaType: "image/png" }, { data: "second", mediaType: "image/webp" }]);
308
+ expect(run).toHaveBeenCalledTimes(1);
309
+ });
310
+
311
+ it("lets automatic mode return text or a hosted image with one driver request", async () => {
312
+ for (const image of [false, true]) {
313
+ const run = vi.fn((options: OpenAIResponsesOptions) => {
314
+ const body = buildOpenAIResponsesRequest(options);
315
+ expect(body.parallel_tool_calls).toBe(false);
316
+ expect(body.tools).toEqual(expect.arrayContaining([{ type: "image_generation" }, expect.objectContaining({ name: "generate_transparent_image" })]));
317
+ return events(...(image ? [{ type: "image" as const, base64: "png", mimeType: "image/png" as const }] : [{ type: "text-delta" as const, text: "hello" }]), { type: "completed", responseId: "r" });
318
+ });
319
+ const d = dispatcher(run);
320
+ await d.onTokenAdded(1, "one", credentials);
321
+ const result = await d.generate(request({ imageGeneration: "auto" }));
322
+ expect(run).toHaveBeenCalledTimes(1);
323
+ expect(image ? result.images?.length : result.text).toBe(image ? 1 : "hello");
324
+ }
325
+ });
326
+
327
+ it("runs one standalone transparent call after completed arguments and preserves driver accounting", async () => {
328
+ const calls: OpenAIResponsesOptions[] = [];
329
+ const signals: Array<AbortSignal | undefined> = [];
330
+ const item = { type: "function_call", call_id: "call", name: "generate_transparent_image", arguments: JSON.stringify({ prompt: "remove background" }) };
331
+ const d = dispatcher((options, signal) => {
332
+ calls.push(options); signals.push(signal);
333
+ if (calls.length === 1) return events(
334
+ { type: "output-item", item: { ...item, arguments: "" }, completed: false },
335
+ { type: "output-item", item, completed: true },
336
+ { type: "output-item", item, completed: true },
337
+ { type: "completed", responseId: "driver", model: "actual-driver", usage: { inputTokens: 10, cachedInputTokens: 2, outputTokens: 3, reasoningTokens: 1, totalTokens: 13 } },
338
+ );
339
+ return events({ type: "image", base64: "png", mimeType: "image/png", generation: { route: "codex-images", model: "gpt-image-2" } }, { type: "completed", responseId: "image", model: "gpt-image-2", usage: { inputTokens: 999, outputTokens: 999, cachedInputTokens: 0, reasoningTokens: 0, totalTokens: 1998 } });
340
+ });
341
+ await d.onTokenAdded(1, "one", credentials);
342
+ const history = [{ type: "message", role: "user", content: [{ type: "input_image", image_url: "https://example.com/ref.png" }] }];
343
+ const result = await d.generate(request({ imageGeneration: "auto", systemPrompt: "system", coldHistory: history, imageGenerationOptions: { outputFormat: "webp" } }));
344
+ expect(calls).toHaveLength(2);
345
+ expect(calls[1]).toMatchObject({ instructions: "system", imageGeneration: { background: "transparent", output_format: "png" } });
346
+ expect(calls[1]!.history[0]).toEqual(history[0]);
347
+ expect(calls[1]!.tools).toBeUndefined();
348
+ expect(calls[1]!.outputSchema).toBeUndefined();
349
+ expect(signals[0]).toBe(signals[1]);
350
+ expect(result).toMatchObject({ model: "actual-driver", usage: { inputTokens: 10, outputTokens: 3, totalTokens: 13 }, images: [{ generation: { model: "gpt-image-2" } }] });
351
+ });
352
+
353
+ it("rejects incomplete, invalid, multiple and mixed automatic image calls before standalone execution", async () => {
354
+ const item = { type: "function_call", call_id: "call", name: "generate_transparent_image", arguments: JSON.stringify({ prompt: "remove background" }) };
355
+ const done = { type: "output-item" as const, item, completed: true };
356
+ const badEvents: OpenAINormalizedEvent[][] = [
357
+ [{ ...done, completed: false }],
358
+ [{ ...done, item: { ...item, arguments: '{"prompt":"x","extra":true}' } }],
359
+ [done, { ...done, item: { ...item, call_id: "second" } }],
360
+ [done, { type: "image", base64: "png", mimeType: "image/png" }],
361
+ [{ type: "output-item", item: { type: "image_generation_call" }, completed: false }],
362
+ ];
363
+ for (const stream of badEvents) {
364
+ const run = vi.fn(() => events(...stream, { type: "completed", responseId: "driver" }));
365
+ const d = dispatcher(run);
366
+ await d.onTokenAdded(1, "one", credentials);
367
+ await expect(d.generate(request({ imageGeneration: "auto" }))).rejects.toMatchObject({ kind: "incomplete" });
368
+ expect(run).toHaveBeenCalledTimes(1);
369
+ }
370
+ });
371
+
372
+ it("rejects structured output in auto mode before transport", async () => {
373
+ const run = vi.fn(() => events());
374
+ const d = dispatcher(run);
375
+ await d.onTokenAdded(1, "one", credentials);
376
+ await expect(d.generate(request({ imageGeneration: "auto", outputSchema: { type: "object" } }))).rejects.toMatchObject({ kind: "gate" });
377
+ expect(run).not.toHaveBeenCalled();
378
+ });
379
+
380
+ it.each(["missing-image", "missing-completion", "cancel"])("rejects standalone %s", async (failure) => {
381
+ const controller = new AbortController();
382
+ let count = 0;
383
+ const d = dispatcher(async function* (_options, signal) {
384
+ count++;
385
+ if (count === 1) {
386
+ yield { type: "output-item", completed: true, item: { type: "function_call", call_id: "call", name: "generate_transparent_image", arguments: '{"prompt":"transparent"}' } };
387
+ yield { type: "completed", responseId: "driver" };
388
+ } else {
389
+ if (failure === "cancel") { controller.abort(); expect(signal?.aborted).toBe(true); throw signal!.reason; }
390
+ if (failure === "missing-image") yield { type: "completed", responseId: "image" };
391
+ else yield { type: "image", base64: "png", mimeType: "image/png" };
392
+ }
393
+ });
394
+ await d.onTokenAdded(1, "one", credentials);
395
+ await expect(d.generate(request({ imageGeneration: "auto", abortSignal: controller.signal }))).rejects.toMatchObject(failure === "cancel" ? { name: "AbortError" } : { kind: "incomplete" });
396
+ expect(count).toBe(2);
397
+ });
398
+
399
+ it("does not start standalone generation after cancellation", async () => {
400
+ const controller = new AbortController();
401
+ const run = vi.fn(async function* () {
402
+ yield { type: "output-item" as const, completed: true, item: { type: "function_call", call_id: "call", name: "generate_transparent_image", arguments: '{"prompt":"transparent"}' } };
403
+ yield { type: "completed" as const, responseId: "driver" };
404
+ controller.abort();
405
+ });
406
+ const d = dispatcher(run);
407
+ await d.onTokenAdded(1, "one", credentials);
408
+ await expect(d.generate(request({ imageGeneration: "auto", abortSignal: controller.signal }))).rejects.toMatchObject({ name: "AbortError" });
409
+ expect(run).toHaveBeenCalledTimes(1);
410
+ });
411
+
298
412
  it("maps images and preserves image failure classifications", async () => {
299
413
  const success = dispatcher(() =>
300
414
  events(
@@ -26,6 +26,7 @@ const logger = getLogger(["qgrid", "openai-dispatcher"]);
26
26
  // 교체된 codex worker 가 강제하던 watchdog. 호출자가 timeoutMs 를 생략해도 요청이
27
27
  // 영원히 매달리지 않도록 direct transport 도 유한한 기본 상한을 유지한다.
28
28
  const DEFAULT_REQUEST_TIMEOUT_MS = 600_000;
29
+ const TRANSPARENT_IMAGE_TOOL = "generate_transparent_image";
29
30
 
30
31
  export type ImageFailureKind = "gate" | "not_called" | "incomplete";
31
32
 
@@ -113,9 +114,48 @@ function asReasoning(req: GenerateRequest): OpenAIResponsesOptions["reasoning"]
113
114
 
114
115
  function requestOptions(req: GenerateRequest): OpenAIResponsesOptions {
115
116
  const { outputFormat, ...imageControls } = req.imageGenerationOptions ?? {};
117
+ const auto = req.imageGeneration === "auto";
118
+ if (auto && req.outputSchema) {
119
+ throw new ImageGenerationError(
120
+ "gate",
121
+ "Automatic image generation does not support structured output or external tools",
122
+ );
123
+ }
116
124
  return {
117
125
  model: req.model ?? "",
118
126
  ...(req.systemPrompt ? { instructions: req.systemPrompt } : {}),
127
+ ...(auto
128
+ ? {
129
+ instructions: [
130
+ req.systemPrompt,
131
+ "Use image_generation for ordinary image generation or editing. Use generate_transparent_image only when a transparent background is requested. Text replies are allowed when no image is needed. Use only one kind of image tool per response, and at most one generate_transparent_image call.",
132
+ ]
133
+ .filter(Boolean)
134
+ .join("\n\n"),
135
+ parallelToolCalls: false,
136
+ tools: [
137
+ {
138
+ type: "function",
139
+ name: TRANSPARENT_IMAGE_TOOL,
140
+ description:
141
+ "Generate or edit an image with a transparent background using the conversation's reference images.",
142
+ strict: true,
143
+ parameters: {
144
+ type: "object",
145
+ properties: {
146
+ prompt: {
147
+ type: "string",
148
+ description:
149
+ "Complete instructions for the transparent image generation or edit.",
150
+ },
151
+ },
152
+ required: ["prompt"],
153
+ additionalProperties: false,
154
+ },
155
+ },
156
+ ],
157
+ }
158
+ : {}),
119
159
  history: inputItems(req),
120
160
  ...(asReasoning(req) ? { reasoning: asReasoning(req) } : {}),
121
161
  ...(req.verbosity ? { verbosity: req.verbosity as OpenAIResponsesOptions["verbosity"] } : {}),
@@ -129,6 +169,7 @@ function requestOptions(req: GenerateRequest): OpenAIResponsesOptions {
129
169
  imageGeneration: req.imageGenerationOptions
130
170
  ? {
131
171
  ...imageControls,
172
+ ...(auto ? { background: "auto" } : {}),
132
173
  ...(outputFormat ? { output_format: outputFormat } : {}),
133
174
  }
134
175
  : true,
@@ -312,6 +353,11 @@ export class OpenAIDispatcher implements ProviderDispatcher {
312
353
  };
313
354
  const images: GeneratedImage[] = [];
314
355
  let imageAttempted = false;
356
+ const auto = req.imageGeneration === "auto";
357
+ const transparentCalls = new Map<string, string>();
358
+ const transparentCallIds = new Set<unknown>();
359
+ let transparentAttempted = false;
360
+ let driverCompleted = false;
315
361
  let servingModel = req.model ?? "";
316
362
  const metadata = selection.metadata;
317
363
  const clientEntry = this.acquireClient(selection);
@@ -334,7 +380,42 @@ export class OpenAIDispatcher implements ProviderDispatcher {
334
380
  });
335
381
  } else if (event.type === "output-item" && event.item.type === "image_generation_call") {
336
382
  imageAttempted = true;
383
+ } else if (auto && event.type === "output-item" && event.item.type === "function_call") {
384
+ if (event.item.name !== TRANSPARENT_IMAGE_TOOL) {
385
+ throw new ImageGenerationError("incomplete", "Unexpected automatic image tool call");
386
+ }
387
+ transparentAttempted = true;
388
+ transparentCallIds.add(event.item.call_id);
389
+ if (!event.completed) continue;
390
+ const id = event.item.call_id;
391
+ let args: unknown;
392
+ try {
393
+ args = JSON.parse(String(event.item.arguments));
394
+ } catch {
395
+ throw new ImageGenerationError("incomplete", "Invalid transparent image arguments");
396
+ }
397
+ if (
398
+ typeof id !== "string" ||
399
+ !id ||
400
+ !args ||
401
+ typeof args !== "object" ||
402
+ Array.isArray(args) ||
403
+ Object.keys(args).length !== 1 ||
404
+ !("prompt" in args) ||
405
+ typeof args.prompt !== "string" ||
406
+ !args.prompt.trim()
407
+ ) {
408
+ throw new ImageGenerationError("incomplete", "Invalid transparent image arguments");
409
+ }
410
+ if (transparentCalls.has(id) && transparentCalls.get(id) !== args.prompt) {
411
+ throw new ImageGenerationError(
412
+ "incomplete",
413
+ "Conflicting transparent image tool calls",
414
+ );
415
+ }
416
+ transparentCalls.set(id, args.prompt);
337
417
  } else if (event.type === "completed" && event.usage) {
418
+ driverCompleted = true;
338
419
  servingModel = event.model ?? servingModel;
339
420
  usage = {
340
421
  totalTokens: event.usage.totalTokens,
@@ -344,6 +425,7 @@ export class OpenAIDispatcher implements ProviderDispatcher {
344
425
  reasoningOutputTokens: event.usage.reasoningTokens,
345
426
  };
346
427
  } else if (event.type === "completed") {
428
+ driverCompleted = true;
347
429
  servingModel = event.model ?? servingModel;
348
430
  } else if (event.type === "error") {
349
431
  if (req.imageGeneration)
@@ -351,10 +433,63 @@ export class OpenAIDispatcher implements ProviderDispatcher {
351
433
  throw event.error;
352
434
  }
353
435
  }
436
+ if (auto && transparentAttempted) {
437
+ if (
438
+ !driverCompleted ||
439
+ transparentCalls.size !== 1 ||
440
+ transparentCallIds.size !== 1 ||
441
+ imageAttempted
442
+ ) {
443
+ throw new ImageGenerationError(
444
+ "incomplete",
445
+ "Expected one completed transparent image call without another image tool",
446
+ );
447
+ }
448
+ if (req.abortSignal?.aborted) throw abortError(req.abortSignal);
449
+ const prompt = [...transparentCalls.values()][0]!;
450
+ let imageCompleted = false;
451
+ for await (const event of clientEntry.client.responses(
452
+ {
453
+ model: req.model ?? "",
454
+ ...(req.systemPrompt ? { instructions: req.systemPrompt } : {}),
455
+ history: [
456
+ ...inputItems(req),
457
+ { type: "message", role: "user", content: [{ type: "input_text", text: prompt }] },
458
+ ],
459
+ imageGeneration: {
460
+ quality: req.imageGenerationOptions?.quality,
461
+ size: req.imageGenerationOptions?.size,
462
+ output_format: "png",
463
+ background: "transparent",
464
+ },
465
+ },
466
+ req.abortSignal,
467
+ )) {
468
+ if (event.type === "image")
469
+ images.push({
470
+ data: event.base64,
471
+ mediaType: event.mimeType,
472
+ revisedPrompt: event.revisedPrompt ?? null,
473
+ ...(event.generation ? { generation: event.generation } : {}),
474
+ });
475
+ else if (event.type === "completed") imageCompleted = true;
476
+ else if (event.type === "error")
477
+ throw new ImageGenerationError("incomplete", event.error.message);
478
+ }
479
+ imageAttempted = true;
480
+ if (!imageCompleted)
481
+ throw new ImageGenerationError(
482
+ "incomplete",
483
+ "Transparent image generation did not complete",
484
+ );
485
+ }
486
+ if (auto && imageAttempted && !driverCompleted)
487
+ throw new ImageGenerationError("incomplete", "Image generation response did not complete");
488
+ if (req.abortSignal?.aborted) throw abortError(req.abortSignal);
354
489
  } finally {
355
490
  this.releaseClient(clientEntry);
356
491
  }
357
- if (req.imageGeneration && images.length === 0) {
492
+ if ((req.imageGeneration === true || (auto && imageAttempted)) && images.length === 0) {
358
493
  throw new ImageGenerationError(
359
494
  imageAttempted ? "incomplete" : "not_called",
360
495
  imageAttempted