ima2-gen 3.13.1 → 3.14.0

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 (157) hide show
  1. package/.env.example +10 -0
  2. package/CHANGELOG.md +14 -0
  3. package/README.md +15 -2
  4. package/bin/commands/capabilities.js +5 -3
  5. package/bin/commands/config.js +15 -4
  6. package/bin/commands/defaults.js +7 -5
  7. package/bin/commands/doctor.js +88 -39
  8. package/bin/commands/gen.js +21 -4
  9. package/bin/commands/models.js +2 -2
  10. package/bin/commands/ping.js +1 -1
  11. package/bin/commands/prompt.js +5 -3
  12. package/bin/commands/service.js +32 -4
  13. package/bin/commands/tools.js +19 -3
  14. package/bin/commands/upscale.js +18 -4
  15. package/bin/commands/video.js +37 -50
  16. package/bin/ima2.js +28 -9
  17. package/bin/lib/characterResolve.js +2 -1
  18. package/bin/lib/client.js +155 -20
  19. package/bin/lib/config-store.js +40 -7
  20. package/bin/lib/doctor-bundle.js +6 -2
  21. package/bin/lib/doctor-checks.js +13 -29
  22. package/bin/lib/doctor-media.js +3 -3
  23. package/bin/lib/doctor-providers.js +48 -35
  24. package/bin/lib/doctor-report.js +78 -0
  25. package/bin/lib/doctor-runtime.js +173 -0
  26. package/bin/lib/mcpJob.js +19 -7
  27. package/bin/lib/output.js +3 -0
  28. package/bin/lib/platform.js +12 -9
  29. package/bin/lib/recover-output.js +9 -2
  30. package/bin/lib/sse.js +9 -1
  31. package/bin/lib/videoMcp.js +16 -8
  32. package/config.js +41 -5
  33. package/docs/API.md +154 -16
  34. package/docs/CLI.md +65 -1
  35. package/docs/DOCKER.md +19 -4
  36. package/docs/DOCKER.zh-CN.md +11 -4
  37. package/docs/DOCKER.zh-TW.md +11 -4
  38. package/docs/FAQ.md +13 -0
  39. package/docs/NPX_QUICKSTART.md +6 -0
  40. package/docs/README.ja.md +15 -2
  41. package/docs/README.ko.md +15 -2
  42. package/docs/README.zh-CN.md +15 -2
  43. package/docs/README.zh-TW.md +15 -2
  44. package/docs/migration/runtime-test-inventory.md +79 -8
  45. package/lib/agyArtifact.js +82 -0
  46. package/lib/agyArtifactRead.js +249 -0
  47. package/lib/agyImageAdapter.js +2 -329
  48. package/lib/agyProcess.js +122 -0
  49. package/lib/apiRequestBudget.js +48 -0
  50. package/lib/assetLifecycle.js +42 -9
  51. package/lib/canvasVersionStore.js +46 -14
  52. package/lib/capabilities.js +7 -1
  53. package/lib/configKeys.js +2 -0
  54. package/lib/errors/providerMap.js +26 -0
  55. package/lib/eventBus.js +10 -6
  56. package/lib/eventsPolicy.js +9 -0
  57. package/lib/geminiApiImageAdapter.js +1 -214
  58. package/lib/generatePipeline.js +39 -165
  59. package/lib/generatedMediaAccess.js +74 -0
  60. package/lib/generationErrors.js +3 -22
  61. package/lib/grokImageAdapter.js +2 -353
  62. package/lib/grokImageCore.js +1 -57
  63. package/lib/grokImageDownload.js +159 -0
  64. package/lib/grokImageDownloadPolicy.js +124 -0
  65. package/lib/grokImagePlanner.js +301 -0
  66. package/lib/grokMultimodeAdapter.js +1 -75
  67. package/lib/grokVideoDownload.js +137 -34
  68. package/lib/inflight.js +112 -112
  69. package/lib/jobs/terminalStore.js +45 -0
  70. package/lib/lanSessionStore.js +131 -0
  71. package/lib/localAccessPolicy.js +180 -0
  72. package/lib/localLanAccess.js +281 -0
  73. package/lib/logger.js +22 -13
  74. package/lib/mcp/downloadMediaResult.js +72 -78
  75. package/lib/multimodePipeline.js +39 -126
  76. package/lib/nodeGeneration.js +44 -101
  77. package/lib/nodeStore.js +25 -11
  78. package/lib/nodeValidation.js +5 -1
  79. package/lib/pinnedHttpGet.js +174 -0
  80. package/lib/promptImport/githubFolder.js +32 -18
  81. package/lib/promptImport/githubSource.js +14 -10
  82. package/lib/promptImport/parsePromptCandidates.js +32 -2
  83. package/lib/providers/adapters/agyOperations.js +179 -0
  84. package/lib/providers/adapters/comfy.js +1 -0
  85. package/lib/providers/adapters/geminiOperations.js +214 -0
  86. package/lib/providers/adapters/googleExecution.js +97 -0
  87. package/lib/providers/adapters/grokExecution.js +137 -0
  88. package/lib/providers/adapters/grokMultimodeOperations.js +83 -0
  89. package/lib/providers/adapters/grokOperations.js +63 -0
  90. package/lib/providers/adapters/openaiExecution.js +118 -0
  91. package/lib/providers/adapters/openaiOperations.js +201 -0
  92. package/lib/providers/adapters/openaiTypes.js +1 -0
  93. package/lib/providers/derive.js +4 -0
  94. package/lib/providers/deriveCore.js +2 -7
  95. package/lib/providers/execution/admission.js +25 -0
  96. package/lib/providers/execution/index.js +34 -0
  97. package/lib/providers/execution/legacy.js +17 -0
  98. package/lib/providers/execution/legacyClassic.js +66 -0
  99. package/lib/providers/execution/legacyEdit.js +30 -0
  100. package/lib/providers/execution/legacyMultimode.js +34 -0
  101. package/lib/providers/execution/legacyNode.js +41 -0
  102. package/lib/providers/execution/types.js +1 -0
  103. package/lib/providers/registry.js +19 -14
  104. package/lib/providers/surfaceSupport.js +22 -0
  105. package/lib/responsesImageAdapter.js +1 -389
  106. package/lib/responsesTransport.js +190 -0
  107. package/lib/spriteJobEvents.js +20 -2
  108. package/lib/ssePublish.js +5 -4
  109. package/lib/videoExtendI2vOperation.js +76 -0
  110. package/package.json +5 -2
  111. package/routes/edit.js +31 -123
  112. package/routes/events.js +140 -63
  113. package/routes/mcpMedia.js +10 -2
  114. package/routes/models.js +28 -25
  115. package/routes/videoExtended.js +9 -68
  116. package/server.js +24 -46
  117. package/ui/dist/.vite/manifest.json +90 -63
  118. package/ui/dist/assets/{AgentWorkspace-PbBytTeV.js → AgentWorkspace-CL3MLv3l.js} +2 -2
  119. package/ui/dist/assets/App-B9cEM6dJ.js +9 -0
  120. package/ui/dist/assets/AssetGenWorkspace-swWPvXrx.js +2 -0
  121. package/ui/dist/assets/AssetsWorkspace-CRVeYU2F.js +1 -0
  122. package/ui/dist/assets/{CardNewsWorkspace-BS3jKOja.js → CardNewsWorkspace-BAOxWLKf.js} +2 -2
  123. package/ui/dist/assets/GenerationRequestLogPanel-DZtYZm-D.js +1 -0
  124. package/ui/dist/assets/HomeWorkspace-BeZ1s0Jm.js +1 -0
  125. package/ui/dist/assets/KeyingPanel-ZyBSXioh.js +1 -0
  126. package/ui/dist/assets/NodeCanvas-BvvcCErf.js +7 -0
  127. package/ui/dist/assets/{PromptBuilderPanel-D97A4pwB.js → PromptBuilderPanel-DBycmShA.js} +2 -2
  128. package/ui/dist/assets/PromptImportDialog-ChvMv7ek.js +2 -0
  129. package/ui/dist/assets/PromptImportDiscoverySection-60py7whH.js +1 -0
  130. package/ui/dist/assets/PromptImportFolderSection-CCRh_-_7.js +1 -0
  131. package/ui/dist/assets/PromptLibraryPanel-BFWQO8OT.js +2 -0
  132. package/ui/dist/assets/SettingsWorkspace-B5Ghsgdj.js +1 -0
  133. package/ui/dist/assets/SpriteRecipeWorkspace-DRWdkmwU.js +1 -0
  134. package/ui/dist/assets/index-7ngEOVxt.js +5 -0
  135. package/ui/dist/assets/index-B5jy7ePg.css +1 -0
  136. package/ui/dist/assets/index-gxh_02DQ.js +22 -0
  137. package/ui/dist/assets/{pptxgen.es-D4pedbwM.js → pptxgen.es-OUNhQLEV.js} +1 -1
  138. package/ui/dist/assets/promptBuilderStore-BTUUzbBT.js +1 -0
  139. package/ui/dist/assets/useAgentDialogFocus-DSc2qY8p.js +1 -0
  140. package/ui/dist/index.html +3 -2
  141. package/ui/dist/assets/AssetGenWorkspace-B-TGlvVb.js +0 -2
  142. package/ui/dist/assets/AssetsWorkspace-LnGaW151.js +0 -1
  143. package/ui/dist/assets/GenerationRequestLogPanel-DuDWD_ic.js +0 -1
  144. package/ui/dist/assets/HomeWorkspace-DVP7UG6O.js +0 -1
  145. package/ui/dist/assets/KeyingPanel-5bRUFQaq.js +0 -1
  146. package/ui/dist/assets/NodeCanvas-BBEpERTb.js +0 -7
  147. package/ui/dist/assets/PromptImportDialog-C89MN5JY.js +0 -2
  148. package/ui/dist/assets/PromptImportDiscoverySection-BgRfDOs2.js +0 -1
  149. package/ui/dist/assets/PromptImportFolderSection-zTMyriT1.js +0 -1
  150. package/ui/dist/assets/PromptLibraryPanel-BD96qZu_.js +0 -2
  151. package/ui/dist/assets/SettingsWorkspace-BAOk3X_8.js +0 -1
  152. package/ui/dist/assets/SpriteRecipeWorkspace-ClIM5QRo.js +0 -1
  153. package/ui/dist/assets/index-0fpfY8vu.css +0 -1
  154. package/ui/dist/assets/index-BFciPGTn.js +0 -5
  155. package/ui/dist/assets/index-BbykeXR9.js +0 -30
  156. package/ui/dist/assets/promptBuilderStore-Do7M8g7J.js +0 -1
  157. package/ui/dist/assets/useAgentDialogFocus-B6-QgEVI.js +0 -1
@@ -0,0 +1,83 @@
1
+ import { errInfo } from "../../errInfo.js";
2
+ import { imageEditPayload, imagePayload, postGrokImages, grokError, downloadGrokImageUrl, } from "../../grokImageCore.js";
3
+ import { planGrokImage } from "../../grokImagePlanner.js";
4
+ import { getGrokProxyBaseUrl } from "../../grokRuntime.js";
5
+ import { logEvent } from "../../logger.js";
6
+ /**
7
+ * Representative-error policy for a multimode run.
8
+ *
9
+ * Items are attempted in order and a mixed-cause run has no principled winner,
10
+ * so the last failure is chosen by construction. The result is only meaningful
11
+ * when every item failed: with any image returned the run is a success and
12
+ * callers must not report an error (lib/multimodePipeline.ts consults it solely
13
+ * under `returned === 0`).
14
+ */
15
+ export function representativeItemError(errors, returnedImages) {
16
+ if (returnedImages > 0)
17
+ return undefined;
18
+ return errors.length > 0 ? errors[errors.length - 1] : undefined;
19
+ }
20
+ export async function generateMultimodeViaGrok(prompt, ctx, options = {}) {
21
+ const model = options.model || ctx.config.grokProvider?.defaultImageModel || "grok-imagine-image-quality";
22
+ const maxGeneratedImages = Math.max(1, Math.trunc(Number(ctx.config.limits?.maxGeneratedImages) || 24));
23
+ const maxImages = Math.min(maxGeneratedImages, Math.max(1, Math.trunc(Number(options.maxImages) || 4)));
24
+ const references = options.references || [];
25
+ const images = [];
26
+ const originalIndexes = [];
27
+ let totalCost = 0;
28
+ let totalWebSearchCalls = 0;
29
+ const itemErrors = [];
30
+ logEvent("grok", "multimode:start", { requestId: options.requestId, model, maxImages, refs: references.length });
31
+ for (let i = 0; i < maxImages; i++) {
32
+ if (options.signal?.aborted)
33
+ throw grokError("Generation canceled", 499, "GENERATION_CANCELED");
34
+ const indexedPrompt = maxImages > 1 ? `[Image ${i + 1} of ${maxImages}] ${prompt}` : prompt;
35
+ const plan = await planGrokImage(indexedPrompt, ctx, {
36
+ model,
37
+ size: options.size,
38
+ signal: options.signal,
39
+ requestId: options.requestId,
40
+ references,
41
+ directApiKey: options.directApiKey,
42
+ webSearchEnabled: options.webSearchEnabled,
43
+ });
44
+ totalWebSearchCalls += plan.webSearchCalls;
45
+ const endpoint = references.length > 0 ? "/v1/images/edits" : "/v1/images/generations";
46
+ const payload = references.length > 0
47
+ ? imageEditPayload(model, plan.prompt, references, options.size)
48
+ : imagePayload(model, plan.prompt, options.size);
49
+ try {
50
+ logEvent("grok", "multimode:item-start", {
51
+ requestId: options.requestId,
52
+ index: i,
53
+ endpoint,
54
+ refs: references.length,
55
+ promptChars: plan.prompt.length,
56
+ });
57
+ const result = await postGrokImages(ctx, payload, options.signal, endpoint, options.directApiKey);
58
+ const imgUrl = result.data?.[0]?.url;
59
+ if (imgUrl) {
60
+ const dl = await downloadGrokImageUrl(imgUrl, options.signal, undefined, {
61
+ trustedProxyOrigin: options.directApiKey
62
+ ? undefined : new URL(getGrokProxyBaseUrl(ctx)).origin,
63
+ });
64
+ const img = { b64: dl.b64, mime: dl.mime, revisedPrompt: plan.prompt, providerUrl: imgUrl };
65
+ images.push(img);
66
+ originalIndexes.push(i);
67
+ if (result.usage?.cost_in_usd_ticks)
68
+ totalCost += result.usage.cost_in_usd_ticks;
69
+ await options.onFinalImage?.(img, i);
70
+ }
71
+ }
72
+ catch (e) {
73
+ if (e.code === "GENERATION_CANCELED")
74
+ throw e;
75
+ logEvent("grok", "multimode:item-error", { requestId: options.requestId, index: i, error: errInfo(e) });
76
+ itemErrors.push(e);
77
+ }
78
+ }
79
+ logEvent("grok", "multimode:done", { requestId: options.requestId, model, returned: images.length, requested: maxImages, refs: references.length });
80
+ const usage = totalCost > 0 ? { grok_cost_usd_ticks: totalCost } : null;
81
+ const representative = representativeItemError(itemErrors, images.length);
82
+ return { images, originalIndexes, usage, webSearchCalls: totalWebSearchCalls, extraIgnored: 0, ...(representative !== undefined ? { error: representative } : {}) };
83
+ }
@@ -0,0 +1,63 @@
1
+ import { logEvent } from "../../logger.js";
2
+ import { detectImageMimeFromB64 } from "../../refs.js";
3
+ import { mapSizeToGrokImageParams } from "../../grokSizeMapper.js";
4
+ import { getGrokProxyBaseUrl } from "../../grokRuntime.js";
5
+ import { planGrokImage } from "../../grokImagePlanner.js";
6
+ import { grokError, imagePayload, imageEditPayload, postGrokImages, downloadGrokImageUrl, } from "../../grokImageCore.js";
7
+ export async function generateViaGrok(prompt, ctx, options = {}) {
8
+ const model = options.model || ctx.config.grokProvider?.defaultImageModel || "grok-imagine-image-quality";
9
+ const references = options.references || [];
10
+ const plan = options.plannedPrompt
11
+ ? { prompt: options.plannedPrompt, model, webSearchCalls: options.webSearchCalls ?? 1 }
12
+ : await planGrokImage(prompt, ctx, { ...options, referenceCount: references.length, directApiKey: options.directApiKey, webSearchEnabled: options.webSearchEnabled });
13
+ const hasReferences = references.length > 0;
14
+ const payload = hasReferences
15
+ ? imageEditPayload(model, plan.prompt, references, options.size)
16
+ : imagePayload(model, plan.prompt, options.size);
17
+ const endpoint = hasReferences ? "/v1/images/edits" : "/v1/images/generations";
18
+ const logStage = hasReferences ? "generate:edit-start" : "generate:start";
19
+ logEvent("grok", logStage, {
20
+ requestId: options.requestId,
21
+ model,
22
+ promptChars: plan.prompt.length,
23
+ size: options.size,
24
+ refs: references.length,
25
+ });
26
+ const result = await postGrokImages(ctx, payload, options.signal, endpoint, options.directApiKey);
27
+ const imageUrl = result.data?.[0]?.url;
28
+ if (!imageUrl) {
29
+ throw grokError("Grok returned no image URL", 502, "GROK_EMPTY_RESPONSE");
30
+ }
31
+ const downloaded = await downloadGrokImageUrl(imageUrl, options.signal, undefined, {
32
+ trustedProxyOrigin: options.directApiKey
33
+ ? undefined : new URL(getGrokProxyBaseUrl(ctx)).origin,
34
+ });
35
+ const usage = result.usage ? { grok_cost_usd_ticks: result.usage.cost_in_usd_ticks ?? 0 } : null;
36
+ logEvent("grok", "generate:done", {
37
+ requestId: options.requestId,
38
+ model,
39
+ endpoint,
40
+ refs: references.length,
41
+ b64Len: downloaded.b64.length,
42
+ });
43
+ return { b64: downloaded.b64, providerUrl: imageUrl, usage, webSearchCalls: plan.webSearchCalls, mime: downloaded.mime, revisedPrompt: plan.prompt };
44
+ }
45
+ export async function editViaGrok(prompt, imageB64, ctx, options = {}) {
46
+ const model = options.model || ctx.config.grokProvider?.defaultImageModel || "grok-imagine-image-quality";
47
+ const detectedInputMime = detectImageMimeFromB64(imageB64) || "image/png";
48
+ const imageUrl = imageB64.startsWith("data:") ? imageB64 : `data:${detectedInputMime};base64,${imageB64}`;
49
+ const payload = { model, prompt, n: 1, response_format: "url", image: { type: "image_url", url: imageUrl }, ...mapSizeToGrokImageParams(options.size) };
50
+ logEvent("grok", "edit:start", { requestId: options.requestId, model, promptChars: prompt.length });
51
+ const result = await postGrokImages(ctx, payload, options.signal, "/v1/images/edits", options.directApiKey);
52
+ const editResultUrl = result.data?.[0]?.url;
53
+ if (!editResultUrl) {
54
+ throw grokError("Grok edit returned no image URL", 502, "GROK_EMPTY_RESPONSE");
55
+ }
56
+ const downloaded = await downloadGrokImageUrl(editResultUrl, options.signal, undefined, {
57
+ trustedProxyOrigin: options.directApiKey
58
+ ? undefined : new URL(getGrokProxyBaseUrl(ctx)).origin,
59
+ });
60
+ const usage = result.usage ? { grok_cost_usd_ticks: result.usage.cost_in_usd_ticks ?? 0 } : null;
61
+ logEvent("grok", "edit:done", { requestId: options.requestId, model, b64Len: downloaded.b64.length });
62
+ return { b64: downloaded.b64, providerUrl: editResultUrl, usage, webSearchCalls: 0, mime: downloaded.mime, revisedPrompt: result.data[0]?.revised_prompt || prompt };
63
+ }
@@ -0,0 +1,118 @@
1
+ import { isNonRetryableGenerationError, normalizeGenerationFailure } from "../../generationErrors.js";
2
+ import { throwIfJobCanceled } from "../../generationCancel.js";
3
+ import { logEvent } from "../../logger.js";
4
+ import { generateViaResponses, editViaResponses, generateMultimodeViaResponses } from "./openaiOperations.js";
5
+ export function isOpenaiRequest(request) {
6
+ return request.provider === "oauth" || request.provider === "api";
7
+ }
8
+ function prepareOpenaiClassic(ctx, request, _progress) {
9
+ const { provider: activeProvider, prompt: generationPrompt, requestId, background: backgroundParams } = request;
10
+ const { model: imageModel, quality, size: effectiveSize, moderation, mode: normalizedPromptMode, reasoningEffort, webSearchEnabled } = request.options;
11
+ // Scalars are captured at prepare; references, signal and ctx stay live per attempt.
12
+ const generateOne = async () => {
13
+ const MAX_RETRIES = 1;
14
+ let lastErr;
15
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
16
+ try {
17
+ const r = await generateViaResponses(activeProvider, generationPrompt, quality, effectiveSize, moderation, request.references, requestId, normalizedPromptMode, ctx, {
18
+ model: imageModel,
19
+ reasoningEffort,
20
+ webSearchEnabled,
21
+ signal: request.signal,
22
+ allowPromptOnlyOAuthFallback: activeProvider !== "api",
23
+ ...(backgroundParams ? { background: backgroundParams.background } : {}),
24
+ ...(backgroundParams?.outputFormat ? { outputFormat: backgroundParams.outputFormat } : {}),
25
+ });
26
+ throwIfJobCanceled(requestId);
27
+ if (r.b64)
28
+ return r;
29
+ lastErr = new Error("Empty response (safety refusal)");
30
+ }
31
+ catch (e) {
32
+ lastErr = e;
33
+ if (isNonRetryableGenerationError(e))
34
+ break;
35
+ }
36
+ if (attempt < MAX_RETRIES) {
37
+ const errCode = (lastErr && typeof lastErr === "object" && "code" in lastErr)
38
+ ? lastErr.code
39
+ : undefined;
40
+ logEvent("generate", "retry", { requestId, attempt: attempt + 1, errorCode: errCode });
41
+ }
42
+ }
43
+ throw normalizeGenerationFailure(lastErr, {
44
+ safetyMessage: "Content generation refused after retries",
45
+ });
46
+ };
47
+ return { execute: async () => {
48
+ try {
49
+ return { kind: "single", value: await generateOne() };
50
+ }
51
+ catch (error) {
52
+ throw error;
53
+ } // Caller owns lifecycle and the existing error envelope.
54
+ } };
55
+ }
56
+ async function executeOpenaiNode(ctx, request, progress) {
57
+ const { provider, sourceImage: parentB64, prompt: generationPrompt, references, requestId, signal, searchMode, options } = request;
58
+ const { model, size, quality, moderation, mode, reasoningEffort, webSearchEnabled } = options;
59
+ const refsForRequest = request.contextMode === "parent-only" ? [] : references;
60
+ return parentB64
61
+ ? await editViaResponses(provider, generationPrompt, parentB64, quality, size, moderation, mode, ctx, requestId, {
62
+ model, references: refsForRequest, searchMode, reasoningEffort, webSearchEnabled, signal,
63
+ })
64
+ : await generateViaResponses(provider, generationPrompt, quality, size, moderation, refsForRequest, requestId, mode, ctx, {
65
+ model, reasoningEffort, webSearchEnabled, signal,
66
+ partialImages: request.partialImages,
67
+ onPartialImage: progress?.onPartialImage ?? null,
68
+ });
69
+ }
70
+ async function executeOpenaiEdit(ctx, request) {
71
+ const { provider, rawPrompt, sourceImage, signal, requestId, options } = request;
72
+ const result = await editViaResponses(provider, rawPrompt, sourceImage, options.quality, options.size, options.moderation, options.mode, ctx, requestId, { model: options.model, reasoningEffort: options.reasoningEffort,
73
+ webSearchEnabled: options.webSearchEnabled,
74
+ ...(request.mask !== null ? { mask: request.mask } : {}), signal });
75
+ // Preserve native retry metadata; only Responses had these caller defaults.
76
+ return { ...result, usage: result.usage ?? null, webSearchCalls: result.webSearchCalls ?? 0 };
77
+ }
78
+ async function executeOpenaiMultimode(ctx, request, progress) {
79
+ const { provider, prompt, references, signal, requestId, options, maxImages } = request;
80
+ return generateMultimodeViaResponses(provider, prompt, options.quality, options.size, options.moderation, references, requestId, options.mode, ctx, { model: options.model, maxImages, reasoningEffort: options.reasoningEffort,
81
+ webSearchEnabled: options.webSearchEnabled,
82
+ ...(progress.onPartialImage !== undefined ? { onPartialImage: progress.onPartialImage } : {}),
83
+ ...(progress.onFinalImage !== undefined ? { onFinalImage: progress.onFinalImage } : {}), signal });
84
+ }
85
+ export async function prepareOpenaiExecution(ctx, request, progress = {}) {
86
+ try {
87
+ switch (request.surface) {
88
+ case "classic": return prepareOpenaiClassic(ctx, request, progress);
89
+ case "node": return { execute: async () => {
90
+ try {
91
+ return { kind: "single", value: await executeOpenaiNode(ctx, request, progress) };
92
+ }
93
+ catch (error) {
94
+ throw error;
95
+ } // Caller owns retries and normalization.
96
+ } };
97
+ case "edit": return { execute: async () => {
98
+ try {
99
+ return { kind: "single", value: await executeOpenaiEdit(ctx, request) };
100
+ }
101
+ catch (error) {
102
+ throw error;
103
+ } // Route owns failure normalization and job cleanup.
104
+ } };
105
+ case "multimode": return { execute: async () => {
106
+ try {
107
+ return { kind: "sequence", value: await executeOpenaiMultimode(ctx, request, progress) };
108
+ }
109
+ catch (error) {
110
+ throw error;
111
+ } // Caller owns partial-timeout recovery.
112
+ } };
113
+ }
114
+ }
115
+ catch (error) {
116
+ throw error;
117
+ }
118
+ }
@@ -0,0 +1,201 @@
1
+ import { compressReferenceB64ForOAuth } from "../../referenceImageCompress.js";
2
+ import { detectImageMimeFromB64 } from "../../refs.js";
3
+ import { requireRuntimeContext } from "../../runtimeContext.js";
4
+ import { imageToolChoice, imageToolChoiceKind, tools, toolTypes, } from "../../responsesTools.js";
5
+ import { emptyResponseError } from "../../responsesErrors.js";
6
+ import { retryPromptOnlyJsonImage } from "../../responsesFallback.js";
7
+ import { AUTO_PROMPT_FIDELITY_SUFFIX, DIRECT_PROMPT_FIDELITY_SUFFIX, EDIT_DEVELOPER_PROMPT, EDIT_NO_SEARCH_DEVELOPER_PROMPT, GENERATE_DEVELOPER_PROMPT, GENERATE_NO_SEARCH_DEVELOPER_PROMPT, MULTIMODE_DEVELOPER_PROMPT, MULTIMODE_NO_SEARCH_DEVELOPER_PROMPT, buildEditTextPrompt, buildMultimodeSequencePrompt, buildUserTextPrompt, } from "../../oauthProxy.js";
8
+ import { postResponses } from "../../responsesTransport.js";
9
+ function normalizeRef(ref) {
10
+ const b64 = typeof ref === "string" ? ref : ref?.b64;
11
+ const detectedMime = typeof ref === "object" && ref?.detectedMime
12
+ ? ref.detectedMime
13
+ : detectImageMimeFromB64(b64);
14
+ const declaredMime = typeof ref === "object" ? ref?.declaredMime : null;
15
+ const mime = ["image/png", "image/jpeg", "image/webp"].includes(detectedMime)
16
+ ? detectedMime
17
+ : ["image/png", "image/jpeg", "image/webp"].includes(declaredMime)
18
+ ? declaredMime
19
+ : "image/png";
20
+ return { type: "input_image", image_url: `data:${mime};base64,${b64}` };
21
+ }
22
+ export async function generateViaResponses(provider, prompt, quality, size, moderation = "low", references = [], requestId = null, mode = "auto", ctxRaw = {}, options = {}) {
23
+ const ctx = requireRuntimeContext(ctxRaw);
24
+ const model = options.model || ctx.config?.imageModels?.default || "gpt-5.6-luna";
25
+ const webSearchEnabled = options.webSearchEnabled !== false && options.searchMode !== "off";
26
+ const requestTools = tools(webSearchEnabled, {
27
+ quality,
28
+ size,
29
+ moderation,
30
+ ...(options.partialImages ? { partial_images: options.partialImages } : {}),
31
+ ...(options.background ? { background: options.background } : {}),
32
+ ...(options.outputFormat ? { output_format: options.outputFormat } : {}),
33
+ });
34
+ const toolChoice = imageToolChoice(options.forceImageToolChoice ?? ctx.config?.oauth?.forceImageToolChoice !== false);
35
+ const toolChoiceKind = imageToolChoiceKind(toolChoice);
36
+ const referenceInputs = references.map(normalizeRef);
37
+ const userContent = referenceInputs.length
38
+ ? [...referenceInputs, { type: "input_text", text: buildUserTextPrompt(prompt, mode, { webSearchEnabled, size }) }]
39
+ : buildUserTextPrompt(prompt, mode, { webSearchEnabled, size });
40
+ const result = await postResponses({
41
+ ctx,
42
+ provider,
43
+ scope: provider === "api" ? "api-generate" : "oauth",
44
+ requestId,
45
+ maxImages: 1,
46
+ ...(options.signal !== undefined ? { signal: options.signal } : {}),
47
+ ...(options.onPartialImage !== undefined ? { onPartialImage: options.onPartialImage } : {}),
48
+ ...(options.onFinalImage !== undefined ? { onFinalImage: options.onFinalImage } : {}),
49
+ payload: {
50
+ model,
51
+ input: [
52
+ { role: "developer", content: webSearchEnabled ? GENERATE_DEVELOPER_PROMPT : GENERATE_NO_SEARCH_DEVELOPER_PROMPT },
53
+ { role: "user", content: userContent },
54
+ ],
55
+ tools: requestTools,
56
+ tool_choice: toolChoice,
57
+ reasoning: { effort: options.reasoningEffort || "low" },
58
+ stream: true,
59
+ },
60
+ });
61
+ const image = result.images[0];
62
+ if (!image?.b64) {
63
+ if (options.allowPromptOnlyOAuthFallback === true) {
64
+ const fallback = await retryPromptOnlyJsonImage({
65
+ postResponses,
66
+ ctx,
67
+ provider,
68
+ prompt,
69
+ mode,
70
+ model,
71
+ quality,
72
+ size,
73
+ moderation,
74
+ requestId,
75
+ ...(options.signal !== undefined ? { signal: options.signal } : {}),
76
+ initial: result,
77
+ referenceInputs,
78
+ webSearchDroppedOnRetry: webSearchEnabled,
79
+ ...(options.reasoningEffort !== undefined ? { reasoningEffort: options.reasoningEffort } : {}),
80
+ ...(options.background !== undefined ? { background: options.background } : {}),
81
+ ...(options.outputFormat !== undefined ? { outputFormat: options.outputFormat } : {}),
82
+ });
83
+ if (fallback)
84
+ return fallback;
85
+ }
86
+ throw emptyResponseError("No image data received from Responses API", result, {
87
+ provider,
88
+ model,
89
+ quality,
90
+ size,
91
+ moderation,
92
+ webSearchEnabled,
93
+ refsCount: referenceInputs.length,
94
+ inputImageCount: referenceInputs.length,
95
+ promptChars: typeof prompt === "string" ? prompt.length : 0,
96
+ toolTypes: toolTypes(requestTools),
97
+ toolChoiceKind,
98
+ });
99
+ }
100
+ return { b64: image.b64, usage: result.usage, webSearchCalls: result.webSearchCalls, revisedPrompt: image.revisedPrompt, text: result.text };
101
+ }
102
+ export async function generateMultimodeViaResponses(provider, prompt, quality, size, moderation = "low", references = [], requestId = null, mode = "auto", ctxRaw = {}, options = {}) {
103
+ const ctx = requireRuntimeContext(ctxRaw);
104
+ const maxGeneratedImages = Math.max(1, Math.trunc(Number(ctx.config.limits.maxGeneratedImages) || 24));
105
+ const maxImages = Math.min(maxGeneratedImages, Math.max(1, Math.trunc(Number(options.maxImages) || 1)));
106
+ const model = options.model || ctx.config?.imageModels?.default || "gpt-5.6-luna";
107
+ const webSearchEnabled = options.webSearchEnabled !== false && options.searchMode !== "off";
108
+ const requestTools = tools(webSearchEnabled, { quality, size, moderation, ...(options.partialImages ? { partial_images: options.partialImages } : {}) });
109
+ const userText = buildMultimodeSequencePrompt(mode === "direct"
110
+ ? `${prompt}${DIRECT_PROMPT_FIDELITY_SUFFIX}`
111
+ : `${prompt}${webSearchEnabled ? "" : ""}${AUTO_PROMPT_FIDELITY_SUFFIX}`, maxImages, { webSearchEnabled, size });
112
+ const referenceInputs = references.map(normalizeRef);
113
+ const userContent = referenceInputs.length
114
+ ? [...referenceInputs, { type: "input_text", text: userText }]
115
+ : userText;
116
+ return await postResponses({
117
+ ctx,
118
+ provider,
119
+ scope: provider === "api" ? "api-multimode" : "oauth-multimode",
120
+ requestId,
121
+ maxImages,
122
+ ...(options.signal !== undefined ? { signal: options.signal } : {}),
123
+ ...(options.onPartialImage !== undefined ? { onPartialImage: options.onPartialImage } : {}),
124
+ ...(options.onFinalImage !== undefined ? { onFinalImage: options.onFinalImage } : {}),
125
+ payload: {
126
+ model,
127
+ input: [
128
+ { role: "developer", content: webSearchEnabled ? MULTIMODE_DEVELOPER_PROMPT : MULTIMODE_NO_SEARCH_DEVELOPER_PROMPT },
129
+ { role: "user", content: userContent },
130
+ ],
131
+ tools: requestTools,
132
+ tool_choice: "required",
133
+ reasoning: { effort: options.reasoningEffort || "low" },
134
+ stream: true,
135
+ },
136
+ });
137
+ }
138
+ export async function editViaResponses(provider, prompt, imageB64, quality, size, moderation = "low", mode = "auto", ctxRaw = {}, requestId = null, options = {}) {
139
+ const ctx = requireRuntimeContext(ctxRaw);
140
+ const model = options.model || ctx.config?.imageModels?.default || "gpt-5.6-luna";
141
+ const webSearchEnabled = options.webSearchEnabled !== false && options.searchMode !== "off";
142
+ const requestTools = tools(webSearchEnabled, { quality, size, moderation });
143
+ const toolChoice = imageToolChoice(options.forceImageToolChoice ?? ctx.config?.oauth?.forceImageToolChoice !== false);
144
+ const toolChoiceKind = imageToolChoiceKind(toolChoice);
145
+ const imageForRequest = await compressReferenceB64ForOAuth(imageB64, {
146
+ maxB64Bytes: ctx.config?.limits?.maxRefB64Bytes,
147
+ force: true,
148
+ });
149
+ const referenceImages = await Promise.all((Array.isArray(options.references) ? options.references : []).map((ref) => compressReferenceB64ForOAuth(typeof ref === "string" ? ref : ref?.b64, {
150
+ maxB64Bytes: ctx.config?.limits?.maxRefB64Bytes,
151
+ force: true,
152
+ })));
153
+ const maskContent = typeof options.mask === "string" && options.mask.length > 0
154
+ ? [
155
+ { type: "input_image", image_url: `data:image/png;base64,${options.mask}` },
156
+ { type: "input_text", text: "The previous image is an edit mask guide. Use it as prompt guidance for where the edit should apply; it is not a visible final image element." },
157
+ ]
158
+ : [];
159
+ const userContent = [
160
+ { type: "input_image", image_url: `data:image/jpeg;base64,${imageForRequest.b64}` },
161
+ ...referenceImages.map(({ b64 }) => ({ type: "input_image", image_url: `data:image/jpeg;base64,${b64}` })),
162
+ ...maskContent,
163
+ { type: "input_text", text: buildEditTextPrompt(prompt, mode, { webSearchEnabled, size }) },
164
+ ];
165
+ const result = await postResponses({
166
+ ctx,
167
+ provider,
168
+ scope: provider === "api" ? "api-edit" : "oauth-edit",
169
+ requestId,
170
+ maxImages: 1,
171
+ signal: options.signal,
172
+ payload: {
173
+ model,
174
+ input: [
175
+ { role: "developer", content: webSearchEnabled ? EDIT_DEVELOPER_PROMPT : EDIT_NO_SEARCH_DEVELOPER_PROMPT },
176
+ { role: "user", content: userContent },
177
+ ],
178
+ tools: requestTools,
179
+ tool_choice: toolChoice,
180
+ reasoning: { effort: options.reasoningEffort || "low" },
181
+ stream: true,
182
+ },
183
+ });
184
+ const image = result.images[0];
185
+ if (!image?.b64) {
186
+ throw emptyResponseError("No image data received from Responses edit", result, {
187
+ provider,
188
+ model,
189
+ quality,
190
+ size,
191
+ moderation,
192
+ webSearchEnabled,
193
+ refsCount: referenceImages.length,
194
+ inputImageCount: 1 + referenceImages.length + (maskContent.length ? 1 : 0),
195
+ promptChars: typeof prompt === "string" ? prompt.length : 0,
196
+ toolTypes: toolTypes(requestTools),
197
+ toolChoiceKind,
198
+ });
199
+ }
200
+ return { b64: image.b64, usage: result.usage, revisedPrompt: image.revisedPrompt, webSearchCalls: result.webSearchCalls };
201
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -2,7 +2,11 @@
2
2
  // in deriveCore.ts; this module binds it to REGISTRY and keeps the narrow
3
3
  // CoreProviderId typing consumers rely on.
4
4
  import { REGISTRY } from "./registry.js";
5
+ import { deriveProviderSurfaceSupportFrom } from "./surfaceSupport.js";
5
6
  import { deriveCliImageModelSetFrom, deriveIdsFrom, deriveImageModelSetFrom, deriveModelsFrom, deriveReferenceLimitFrom, deriveReferenceLimitMapFrom, deriveSupportedImageModelsFrom, deriveUnsupportedImageModelsFrom, } from "./deriveCore.js";
7
+ export function getProviderSurfaceSupport(providerId, surface) {
8
+ return deriveProviderSurfaceSupportFrom(REGISTRY, providerId, surface);
9
+ }
6
10
  export function deriveProviderIds(registry = REGISTRY) {
7
11
  return deriveIdsFrom(registry);
8
12
  }
@@ -7,10 +7,10 @@ export function deriveModelsFrom(registry, providerId, kind) {
7
7
  }
8
8
  export function deriveSupportedImageModelsFrom(registry, providerId) {
9
9
  const provider = registry.find((entry) => entry.id === providerId);
10
- return new Set(provider?.models.filter((model) => model.kind === "image" && supportsAnything(model.supports)).map((model) => model.id) ?? []);
10
+ return new Set(provider?.models.filter((model) => model.kind === "image" && model.supports.generate).map((model) => model.id) ?? []);
11
11
  }
12
12
  export function deriveUnsupportedImageModelsFrom(registry) {
13
- return new Set(registry.flatMap((provider) => provider.models.filter((model) => model.kind === "image" && !supportsAnything(model.supports)).map((model) => model.id)));
13
+ return new Set(registry.flatMap((provider) => provider.models.filter((model) => model.kind === "image" && !model.supports.generate).map((model) => model.id)));
14
14
  }
15
15
  export function deriveImageModelSetFrom(registry) {
16
16
  return new Set(registry.flatMap((provider) => provider.models.filter((model) => model.kind === "image").map((model) => model.id)));
@@ -27,8 +27,3 @@ export function deriveReferenceLimitMapFrom(registry, mode) {
27
27
  export function deriveReferenceLimitFrom(registry, providerId, mode) {
28
28
  return registry.find((entry) => entry.id === providerId)?.referenceLimits[mode];
29
29
  }
30
- // A model with no capability at all is an explicitly unsupported entry
31
- // (gpt-5.3-codex-spark), which lib/imageModels.ts rejects by name.
32
- function supportsAnything(supports) {
33
- return Boolean(supports.edit || supports.mask || supports.streaming);
34
- }
@@ -0,0 +1,25 @@
1
+ import { getProviderSurfaceSupport } from "../derive.js";
2
+ function directKeyFailure(ctx, provider) {
3
+ if (provider !== "grok-api" || (typeof ctx.xaiApiKey === "string" && ctx.xaiApiKey.trim()))
4
+ return null;
5
+ return { status: 401, code: "GROK_API_KEY_MISSING",
6
+ message: "Grok API key is required for grok-api image generation" };
7
+ }
8
+ export function checkImageExecutionAdmission(ctx, input) {
9
+ const missingKey = directKeyFailure(ctx, input.provider);
10
+ if (missingKey)
11
+ return missingKey;
12
+ const surface = input.surface === "classic" ? "generate" : input.surface;
13
+ if (input.provider === "nai" && surface === "multimode" && input.referenceCount > 0
14
+ && getProviderSurfaceSupport(input.provider, surface)?.references === false) {
15
+ return { status: 400, code: "NAI_REF_UNSUPPORTED",
16
+ message: "NovelAI image generation does not accept reference images yet" };
17
+ }
18
+ return null;
19
+ }
20
+ /** Recheck mutable direct credentials without changing the provider or captured key. */
21
+ export function assertDirectGrokKey(ctx, provider) {
22
+ const failure = directKeyFailure(ctx, provider);
23
+ if (failure)
24
+ throw Object.assign(new Error(failure.message), { status: failure.status, code: failure.code });
25
+ }
@@ -0,0 +1,34 @@
1
+ import { assertDirectGrokKey } from "./admission.js";
2
+ import { isLegacyExecutionRequest, prepareLegacyImageExecution } from "./legacy.js";
3
+ import { isOpenaiRequest, prepareOpenaiExecution } from "../adapters/openaiExecution.js";
4
+ import { isGrokRequest, prepareGrokExecution } from "../adapters/grokExecution.js";
5
+ import { isGoogleRequest, prepareGoogleExecution } from "../adapters/googleExecution.js";
6
+ function prepareSelected(ctx, request, progress) {
7
+ if (isOpenaiRequest(request))
8
+ return prepareOpenaiExecution(ctx, request, progress);
9
+ if (isGrokRequest(request))
10
+ return prepareGrokExecution(ctx, request, progress);
11
+ if (isGoogleRequest(request))
12
+ return prepareGoogleExecution(ctx, request, progress);
13
+ if (isLegacyExecutionRequest(request))
14
+ return prepareLegacyImageExecution(ctx, request, progress);
15
+ throw new Error("Unreachable image execution provider");
16
+ }
17
+ export async function prepareImageExecution(ctx, request, progress) {
18
+ try {
19
+ assertDirectGrokKey(ctx, request.provider);
20
+ const prepared = await prepareSelected(ctx, request, progress);
21
+ return { execute: async () => {
22
+ try {
23
+ assertDirectGrokKey(ctx, request.provider);
24
+ return await prepared.execute();
25
+ }
26
+ catch (error) {
27
+ throw error;
28
+ } // Preserve transport identity; callers own normalization.
29
+ } };
30
+ }
31
+ catch (error) {
32
+ throw error;
33
+ }
34
+ }
@@ -0,0 +1,17 @@
1
+ import { prepareLegacyClassic } from "./legacyClassic.js";
2
+ import { prepareLegacyNode } from "./legacyNode.js";
3
+ import { prepareLegacyEdit } from "./legacyEdit.js";
4
+ import { prepareLegacyMultimode } from "./legacyMultimode.js";
5
+ export function isLegacyExecutionRequest(request) {
6
+ return request.provider !== "oauth" && request.provider !== "api"
7
+ && request.provider !== "grok" && request.provider !== "grok-api"
8
+ && request.provider !== "agy" && request.provider !== "gemini-api";
9
+ }
10
+ export function prepareLegacyImageExecution(ctx, request, progress) {
11
+ switch (request.surface) {
12
+ case "classic": return prepareLegacyClassic(ctx, request, progress);
13
+ case "node": return prepareLegacyNode(ctx, request, progress);
14
+ case "edit": return prepareLegacyEdit(ctx, request, progress);
15
+ case "multimode": return prepareLegacyMultimode(ctx, request, progress);
16
+ }
17
+ }
@@ -0,0 +1,66 @@
1
+ import { generateViaAtlasCloud } from "../../atlasCloudImageAdapter.js";
2
+ import { generateViaMinimax } from "../../minimaxImageAdapter.js";
3
+ import { generateViaNai } from "../../naiImageAdapter.js";
4
+ import { generateViaComfy } from "../../comfyImageAdapter.js";
5
+ import { requireRuntimeContext } from "../../runtimeContext.js";
6
+ function captureClassicRequest(request) {
7
+ const { provider: activeProvider, prompt: generationPrompt, requestId, background: backgroundParams } = request;
8
+ const { model: imageModel, quality, size: effectiveSize } = request.options;
9
+ return { activeProvider, generationPrompt, requestId, backgroundParams, imageModel, quality, effectiveSize };
10
+ }
11
+ async function executeLegacyClassic(ctx, request, progress, captured) {
12
+ try {
13
+ const { activeProvider, generationPrompt, requestId, backgroundParams, imageModel, quality, effectiveSize } = captured;
14
+ if (activeProvider === "atlascloud") {
15
+ return await generateViaAtlasCloud(generationPrompt, requireRuntimeContext(ctx), {
16
+ model: imageModel, size: effectiveSize, quality, signal: request.signal,
17
+ requestId, references: request.references,
18
+ ...(backgroundParams ? { background: backgroundParams.background } : {}),
19
+ // The caller's resolveImageBackgroundParams validates this alpha-only format.
20
+ ...(backgroundParams?.outputFormat ? { outputFormat: backgroundParams.outputFormat } : {}),
21
+ });
22
+ }
23
+ if (activeProvider === "minimax") {
24
+ return await generateViaMinimax(generationPrompt, requireRuntimeContext(ctx), {
25
+ model: imageModel, size: effectiveSize, signal: request.signal,
26
+ requestId, references: request.references,
27
+ });
28
+ }
29
+ if (activeProvider === "nai") {
30
+ // Text-to-image only: the caller already refused references.
31
+ return await generateViaNai(generationPrompt, requireRuntimeContext(ctx), {
32
+ model: imageModel, size: effectiveSize, signal: request.signal, requestId,
33
+ // Reuse the same normalized options as node and multimode.
34
+ ...request.nai,
35
+ });
36
+ }
37
+ if (activeProvider === "comfy") {
38
+ return await generateViaComfy(generationPrompt, requireRuntimeContext(ctx), {
39
+ model: imageModel, size: effectiveSize, signal: request.signal,
40
+ requestId, references: request.references,
41
+ ...request.comfy, onQueue: progress.onQueue,
42
+ });
43
+ }
44
+ throw new Error(`Unsupported classic execution provider: ${activeProvider}`);
45
+ }
46
+ catch (error) {
47
+ throw error;
48
+ }
49
+ }
50
+ export async function prepareLegacyClassic(ctx, request, progress = {}) {
51
+ try {
52
+ // Preserve scalar/background capture; ctx, refs, signal and lane options stay live.
53
+ const captured = captureClassicRequest(request);
54
+ return { execute: async () => {
55
+ try {
56
+ return { kind: "single", value: await executeLegacyClassic(ctx, request, progress, captured) };
57
+ }
58
+ catch (error) {
59
+ throw error;
60
+ } // Caller owns lifecycle and the existing error envelope.
61
+ } };
62
+ }
63
+ catch (error) {
64
+ throw error;
65
+ }
66
+ }