ima2-gen 2.0.15 → 2.0.16
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/README.md +30 -0
- package/bin/commands/skill.js +336 -17
- package/bin/ima2.js +22 -2
- package/bin/lib/doctor-checks.js +6 -4
- package/config.js +1 -0
- package/docs/CLI.md +25 -3
- package/docs/PROMPT_STUDIO.ko.md +29 -0
- package/docs/PROMPT_STUDIO.md +31 -0
- package/docs/migration/runtime-test-inventory.md +7 -1
- package/lib/agentImageVideoGen.js +8 -0
- package/lib/agentQueueStore.js +36 -10
- package/lib/agentQueueWorker.js +79 -7
- package/lib/agentRuntime.js +2 -0
- package/lib/assetLifecycle.js +40 -8
- package/lib/canvasVersionStore.js +86 -0
- package/lib/cardNewsGenerator.js +37 -3
- package/lib/cardNewsJobStore.js +10 -0
- package/lib/cardNewsManifestStore.js +6 -12
- package/lib/cardNewsPath.js +25 -0
- package/lib/db.js +2 -0
- package/lib/generatePipeline.js +474 -0
- package/lib/generationInputValidation.js +32 -0
- package/lib/grokImageCore.js +18 -1
- package/lib/historyIndex.js +11 -4
- package/lib/historyList.js +3 -0
- package/lib/imageMetadata.js +3 -0
- package/lib/inflight.js +30 -3
- package/lib/multimodePipeline.js +462 -0
- package/lib/nodeGeneration.js +440 -0
- package/lib/nodeValidation.js +23 -0
- package/lib/oauthProxy/generators.js +3 -246
- package/lib/oauthProxy/multimodeGenerators.js +251 -0
- package/lib/routeHelpers.js +9 -2
- package/package.json +2 -2
- package/routes/agent.js +4 -4
- package/routes/canvasVersions.js +21 -1
- package/routes/cardNews.js +2 -1
- package/routes/edit.js +25 -9
- package/routes/events.js +5 -1
- package/routes/generate.js +2 -481
- package/routes/keys.js +31 -39
- package/routes/multimode.js +2 -502
- package/routes/nodes.js +3 -448
- package/routes/video.js +12 -3
- package/routes/videoExtended.js +19 -4
- package/server.js +53 -1
- package/skills/ima2/SKILL.md +204 -0
- package/skills/ima2-front/SKILL.md +611 -0
- package/skills/ima2-front/references/a11y-patterns.md +134 -0
- package/skills/ima2-front/references/aesthetics.md +327 -0
- package/skills/ima2-front/references/anti-slop.md +241 -0
- package/skills/ima2-front/references/asset-requirements.md +431 -0
- package/skills/ima2-front/references/brand-asset-sourcing.md +122 -0
- package/skills/ima2-front/references/color-system.md +132 -0
- package/skills/ima2-front/references/consistency-locks.md +20 -0
- package/skills/ima2-front/references/crud-ui.md +53 -0
- package/skills/ima2-front/references/dropdown-layer.md +110 -0
- package/skills/ima2-front/references/i18n-global.md +104 -0
- package/skills/ima2-front/references/iterative-design.md +90 -0
- package/skills/ima2-front/references/korea-2026.md +174 -0
- package/skills/ima2-front/references/layout-discipline.md +161 -0
- package/skills/ima2-front/references/liquid-glass.md +181 -0
- package/skills/ima2-front/references/logo-sections.md +194 -0
- package/skills/ima2-front/references/mobile-ux.md +144 -0
- package/skills/ima2-front/references/performance-budget.md +106 -0
- package/skills/ima2-front/references/preflight-full.md +58 -0
- package/skills/ima2-front/references/product-density.md +53 -0
- package/skills/ima2-front/references/prototype-variants.md +54 -0
- package/skills/ima2-front/references/reference-capture.md +120 -0
- package/skills/ima2-front/references/responsive-viewport.md +139 -0
- package/skills/ima2-front/references/seo-baseline.md +123 -0
- package/skills/ima2-front/references/soft-3d-asset-gates.md +70 -0
- package/skills/ima2-front/references/stacks/astro.md +204 -0
- package/skills/ima2-front/references/stacks/mobile-native.md +281 -0
- package/skills/ima2-front/references/stacks/nextjs.md +219 -0
- package/skills/ima2-front/references/stacks/react.md +264 -0
- package/skills/ima2-front/references/stacks/svelte.md +235 -0
- package/skills/ima2-front/references/stacks/vanilla.md +226 -0
- package/skills/ima2-front/references/theme-switching.md +82 -0
- package/skills/ima2-front/references/top-bar.md +126 -0
- package/skills/ima2-front/references/typography-wrapping.md +270 -0
- package/skills/ima2-front/references/ux-writing-ko.md +62 -0
- package/skills/ima2-front/references/visual-verification.md +102 -0
- package/skills/ima2-uiux/SKILL.md +477 -0
- package/skills/ima2-uiux/references/anti-slop-judgment.md +39 -0
- package/skills/ima2-uiux/references/color-system.md +80 -0
- package/skills/ima2-uiux/references/design-isms.md +194 -0
- package/skills/ima2-uiux/references/design-read-example.md +33 -0
- package/skills/ima2-uiux/references/design-system-bootstrap.md +133 -0
- package/skills/ima2-uiux/references/favicon-logo.md +355 -0
- package/skills/ima2-uiux/references/form-patterns.md +118 -0
- package/skills/ima2-uiux/references/intent-discovery-ladder.md +86 -0
- package/skills/ima2-uiux/references/korean-design-vocabulary.md +52 -0
- package/skills/ima2-uiux/references/layout-macrostructures.md +54 -0
- package/skills/ima2-uiux/references/logo-trust-sections.md +96 -0
- package/skills/ima2-uiux/references/mobile-native-ux.md +207 -0
- package/skills/ima2-uiux/references/product-personalities.md +118 -0
- package/skills/ima2-uiux/references/responsive-nav.md +66 -0
- package/skills/ima2-uiux/references/typography-line-breaks.md +193 -0
- package/skills/ima2-uiux/references/ux-preflight.md +65 -0
- package/skills/ima2-uiux/references/ux-states.md +197 -0
- package/skills/ima2-uiux/references/visual-hierarchy.md +115 -0
- package/ui/dist/.vite/manifest.json +13 -13
- package/ui/dist/assets/AgentWorkspace-CtYt4SF5.js +3 -0
- package/ui/dist/assets/{CardNewsWorkspace-Cdxn1ohU.js → CardNewsWorkspace-DN5cVqG5.js} +1 -1
- package/ui/dist/assets/GenerationRequestLogPanel-DnmUQdKR.js +1 -0
- package/ui/dist/assets/{NodeCanvas-C67sLboA.js → NodeCanvas-CxpfIBI0.js} +1 -1
- package/ui/dist/assets/PromptBuilderPanel-C2R3ObtR.js +2 -0
- package/ui/dist/assets/{PromptImportDialog-DeQXsEEv.js → PromptImportDialog-BIxz_yEx.js} +2 -2
- package/ui/dist/assets/PromptImportDiscoverySection-BQTlPMME.js +1 -0
- package/ui/dist/assets/PromptImportFolderSection-Ds_Th9O3.js +1 -0
- package/ui/dist/assets/{PromptLibraryPanel-Db3xEPM_.js → PromptLibraryPanel-Rs4l45pV.js} +2 -2
- package/ui/dist/assets/{SettingsWorkspace-BDk_1W-V.js → SettingsWorkspace-p1AZ6uT9.js} +1 -1
- package/ui/dist/assets/index-0-_vgFGs.js +4 -0
- package/ui/dist/assets/index-Dm3pFxV4.js +23 -0
- package/ui/dist/assets/index-J8yDF3Ch.css +1 -0
- package/ui/dist/index.html +2 -2
- package/ui/dist/assets/AgentWorkspace-DAg_7zpY.js +0 -3
- package/ui/dist/assets/GenerationRequestLogPanel-C_qvEUi2.js +0 -1
- package/ui/dist/assets/PromptBuilderPanel-DjvVdAie.js +0 -2
- package/ui/dist/assets/PromptImportDiscoverySection-CrwyUKYI.js +0 -1
- package/ui/dist/assets/PromptImportFolderSection-CToGMqYE.js +0 -1
- package/ui/dist/assets/index-2oG6in1i.css +0 -1
- package/ui/dist/assets/index-CoxMOXAU.js +0 -23
- package/ui/dist/assets/index-p8egnGHk.js +0 -4
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { isAbsolute, resolve, sep } from "node:path";
|
|
2
|
+
export function assertSafeSetId(setId, status = 400) {
|
|
3
|
+
if (typeof setId === "string"
|
|
4
|
+
&& !isAbsolute(setId)
|
|
5
|
+
&& !setId.includes("..")
|
|
6
|
+
&& !setId.includes("/")
|
|
7
|
+
&& !setId.includes("\\")
|
|
8
|
+
&& /^[a-zA-Z0-9_-]{3,120}$/.test(setId))
|
|
9
|
+
return setId;
|
|
10
|
+
const err = new Error(status === 404 ? "Card News set not found" : "Invalid Card News setId");
|
|
11
|
+
err.status = status;
|
|
12
|
+
err.code = status === 404 ? "CARD_NEWS_SET_NOT_FOUND" : "INVALID_CARD_NEWS_SET_ID";
|
|
13
|
+
throw err;
|
|
14
|
+
}
|
|
15
|
+
export function resolveCardNewsSetDir(generatedDir, setId, status = 400) {
|
|
16
|
+
const root = resolve(generatedDir, "cardnews");
|
|
17
|
+
const target = resolve(root, assertSafeSetId(setId, status));
|
|
18
|
+
if (!target.startsWith(root + sep)) {
|
|
19
|
+
const err = new Error("Invalid Card News setId");
|
|
20
|
+
err.status = status;
|
|
21
|
+
err.code = status === 404 ? "CARD_NEWS_SET_NOT_FOUND" : "INVALID_CARD_NEWS_SET_ID";
|
|
22
|
+
throw err;
|
|
23
|
+
}
|
|
24
|
+
return target;
|
|
25
|
+
}
|
package/lib/db.js
CHANGED
|
@@ -151,6 +151,7 @@ function migrate(database) {
|
|
|
151
151
|
result_image_ids TEXT NOT NULL DEFAULT '[]',
|
|
152
152
|
error_code TEXT,
|
|
153
153
|
error_message TEXT,
|
|
154
|
+
progress_stage TEXT,
|
|
154
155
|
created_at INTEGER NOT NULL,
|
|
155
156
|
started_at INTEGER,
|
|
156
157
|
finished_at INTEGER,
|
|
@@ -201,6 +202,7 @@ function migrate(database) {
|
|
|
201
202
|
addColumnIfMissing(database, agentQueueColumns, "agent_queue_items", "result_image_ids", "TEXT NOT NULL DEFAULT '[]'");
|
|
202
203
|
addColumnIfMissing(database, agentQueueColumns, "agent_queue_items", "error_code", "TEXT");
|
|
203
204
|
addColumnIfMissing(database, agentQueueColumns, "agent_queue_items", "error_message", "TEXT");
|
|
205
|
+
addColumnIfMissing(database, agentQueueColumns, "agent_queue_items", "progress_stage", "TEXT");
|
|
204
206
|
addColumnIfMissing(database, agentQueueColumns, "agent_queue_items", "started_at", "INTEGER");
|
|
205
207
|
addColumnIfMissing(database, agentQueueColumns, "agent_queue_items", "finished_at", "INTEGER");
|
|
206
208
|
// ── Prompt Library (schema v4) ──
|
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
2
|
+
import { safeWriteSidecar, atomicWriteJson } from "./atomicWrite.js";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { randomBytes } from "crypto";
|
|
5
|
+
import { detectImageMimeFromB64, summarizeReferencePayload, validateAndNormalizeRefs } from "./refs.js";
|
|
6
|
+
import { generateImageThumbnailFromBuffer } from "./imageThumb.js";
|
|
7
|
+
import { classifyUpstreamError } from "./errorClassify.js";
|
|
8
|
+
import { appendGenerationRequestLog } from "./generationRequestLog.js";
|
|
9
|
+
import { normalizeOAuthParams } from "./oauthNormalize.js";
|
|
10
|
+
import { resolveProviderOptions } from "./providerOptions.js";
|
|
11
|
+
import { generateViaResponses } from "./responsesImageAdapter.js";
|
|
12
|
+
import { generateViaGrok, planGrokImage } from "./grokImageAdapter.js";
|
|
13
|
+
import { generateViaAgy } from "./agyImageAdapter.js";
|
|
14
|
+
import { generateViaGeminiApi } from "./geminiApiImageAdapter.js";
|
|
15
|
+
import { isNonRetryableGenerationError, normalizeGenerationFailure } from "./generationErrors.js";
|
|
16
|
+
import { startJob, finishJob, registerJobAbortController, isJobCanceled, isStartJobFailure, setJobPhase, INFLIGHT_RETRY_AFTER_SECONDS, } from "./inflight.js";
|
|
17
|
+
import { isGenerationCanceledError, makeGenerationCanceledError, throwIfJobCanceled, } from "./generationCancel.js";
|
|
18
|
+
import { logEvent, logError } from "./logger.js";
|
|
19
|
+
import { embedImageMetadataBestEffort } from "./imageMetadataStore.js";
|
|
20
|
+
import { invalidateHistoryIndex } from "./historyIndex.js";
|
|
21
|
+
import { normalizeComposerInsertedPrompts, normalizeComposerPrompt, } from "./composerSnapshot.js";
|
|
22
|
+
import { errInfo } from "./errInfo.js";
|
|
23
|
+
import { requireRuntimeContext } from "./runtimeContext.js";
|
|
24
|
+
import { STORYBOARD_PREFIX } from "./storyboardPrefix.js";
|
|
25
|
+
import { validateModeration, imageFormatFromMime, upstreamErrorFields } from "./routeHelpers.js";
|
|
26
|
+
import { publish } from "./eventBus.js";
|
|
27
|
+
import { publishJobEvent } from "./ssePublish.js";
|
|
28
|
+
import { normalizeBodyRequestId, validateBoundedCount, validateGenerationPrompt } from "./generationInputValidation.js";
|
|
29
|
+
export async function runGeneratePipeline(req, res, ctx) {
|
|
30
|
+
const requestId = normalizeBodyRequestId(req.body?.requestId, req.id);
|
|
31
|
+
const asyncMode = req.body?.async === true;
|
|
32
|
+
let finishStatus = "completed";
|
|
33
|
+
let finishHttpStatus;
|
|
34
|
+
let finishErrorCode;
|
|
35
|
+
let finishMeta = {};
|
|
36
|
+
let finishCanceled = false;
|
|
37
|
+
let jobOwned = false;
|
|
38
|
+
const cancelController = new AbortController();
|
|
39
|
+
const fail = (status, payload) => {
|
|
40
|
+
finishStatus = "error";
|
|
41
|
+
finishHttpStatus = status;
|
|
42
|
+
finishErrorCode = typeof payload.code === "string" ? payload.code : finishErrorCode;
|
|
43
|
+
if (asyncMode && res.headersSent) {
|
|
44
|
+
publish(requestId, "error", { ...payload, status, requestId });
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
return res.status(status).json(payload);
|
|
48
|
+
};
|
|
49
|
+
const succeed = (payload) => {
|
|
50
|
+
if (asyncMode) {
|
|
51
|
+
publishJobEvent(requestId, "done", payload);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
res.json(payload);
|
|
55
|
+
};
|
|
56
|
+
try {
|
|
57
|
+
const sessionId = typeof req.body?.sessionId === "string" ? req.body.sessionId : null;
|
|
58
|
+
const clientNodeId = typeof req.body?.clientNodeId === "string" ? req.body.clientNodeId : null;
|
|
59
|
+
const { prompt, quality: rawQuality = "medium", size = "1024x1024", format = "png", moderation = "low", provider = "auto", n = 1, references = [], mode: promptMode = "auto", model: rawModel, reasoningEffort: rawReasoningEffort, webSearchEnabled: rawWebSearchEnabled = true, } = req.body;
|
|
60
|
+
const promptError = validateGenerationPrompt(prompt);
|
|
61
|
+
if (promptError)
|
|
62
|
+
return fail(400, promptError);
|
|
63
|
+
const maxGeneratedImages = Math.max(1, Math.trunc(Number(ctx.config.limits.maxGeneratedImages) || 1));
|
|
64
|
+
const countResult = validateBoundedCount(n, 1, maxGeneratedImages, "n");
|
|
65
|
+
if ("error" in countResult)
|
|
66
|
+
return fail(400, countResult);
|
|
67
|
+
const count = countResult.value;
|
|
68
|
+
const storyboardActive = req.body?.storyboard === true;
|
|
69
|
+
const storyboardPrefix = storyboardActive ? STORYBOARD_PREFIX : "";
|
|
70
|
+
const composerPrompt = normalizeComposerPrompt(req.body?.composerPrompt);
|
|
71
|
+
const composerInsertedPrompts = normalizeComposerInsertedPrompts(req.body?.composerInsertedPrompts);
|
|
72
|
+
const { quality, warnings: qualityWarnings } = normalizeOAuthParams({ provider, quality: rawQuality });
|
|
73
|
+
const providerOptions = resolveProviderOptions(ctx, {
|
|
74
|
+
provider,
|
|
75
|
+
rawModel,
|
|
76
|
+
rawReasoningEffort,
|
|
77
|
+
rawSize: size,
|
|
78
|
+
rawWebSearchEnabled,
|
|
79
|
+
});
|
|
80
|
+
if (providerOptions.error) {
|
|
81
|
+
return fail(providerOptions.status, { error: providerOptions.error, code: providerOptions.code });
|
|
82
|
+
}
|
|
83
|
+
const imageModel = providerOptions.model;
|
|
84
|
+
const reasoningEffort = providerOptions.reasoningEffort;
|
|
85
|
+
const effectiveSize = providerOptions.size;
|
|
86
|
+
const webSearchEnabled = providerOptions.webSearchEnabled;
|
|
87
|
+
const activeProvider = providerOptions.provider;
|
|
88
|
+
const normalizedPromptMode = promptMode === "direct" ? "direct" : "auto";
|
|
89
|
+
const generationPrompt = storyboardPrefix + prompt;
|
|
90
|
+
const moderationCheck = validateModeration(ctx, moderation);
|
|
91
|
+
if (moderationCheck.error)
|
|
92
|
+
return fail(400, { error: moderationCheck.error });
|
|
93
|
+
const referencePayload = summarizeReferencePayload(references);
|
|
94
|
+
const refCheckResult = validateAndNormalizeRefs(references);
|
|
95
|
+
if (refCheckResult.error) {
|
|
96
|
+
return fail(400, { error: refCheckResult.error, code: refCheckResult.code });
|
|
97
|
+
}
|
|
98
|
+
const refCheck = refCheckResult;
|
|
99
|
+
const incomingProviderUrl = typeof req.body?.providerUrl === "string" && req.body.providerUrl.startsWith("http")
|
|
100
|
+
? req.body.providerUrl
|
|
101
|
+
: null;
|
|
102
|
+
const grokRefs = incomingProviderUrl
|
|
103
|
+
? [{ b64: "", url: incomingProviderUrl, declaredMime: "image/png", detectedMime: "image/png" }, ...refCheck.refDetails]
|
|
104
|
+
: refCheck.refDetails;
|
|
105
|
+
const providerRefCount = activeProvider === "grok" || activeProvider === "grok-api"
|
|
106
|
+
? grokRefs.length
|
|
107
|
+
: refCheck.refs.length;
|
|
108
|
+
if ((activeProvider === "grok" || activeProvider === "agy" || activeProvider === "grok-api" || activeProvider === "gemini-api") && providerRefCount > 3) {
|
|
109
|
+
return fail(400, {
|
|
110
|
+
error: `${activeProvider === "agy" ? "Agy" : "Grok"} image editing supports up to 3 reference images`,
|
|
111
|
+
code: activeProvider === "agy" ? "AGY_REF_TOO_MANY" : "GROK_REF_TOO_MANY",
|
|
112
|
+
requestId,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
const started = startJob({
|
|
116
|
+
requestId,
|
|
117
|
+
kind: "classic",
|
|
118
|
+
prompt,
|
|
119
|
+
meta: {
|
|
120
|
+
kind: "classic",
|
|
121
|
+
sessionId,
|
|
122
|
+
parentNodeId: null,
|
|
123
|
+
clientNodeId,
|
|
124
|
+
quality,
|
|
125
|
+
model: imageModel,
|
|
126
|
+
size: effectiveSize,
|
|
127
|
+
n: count,
|
|
128
|
+
refsCount: providerRefCount,
|
|
129
|
+
referenceBytes: referencePayload.referenceBytes,
|
|
130
|
+
referenceB64Chars: referencePayload.referenceB64Chars,
|
|
131
|
+
composerPrompt,
|
|
132
|
+
composerInsertedPrompts,
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
if (started && isStartJobFailure(started)) {
|
|
136
|
+
const status = started.code === "TOO_MANY_JOBS" ? 429 : 409;
|
|
137
|
+
if (started.code === "TOO_MANY_JOBS") {
|
|
138
|
+
res.setHeader("Retry-After", String(INFLIGHT_RETRY_AFTER_SECONDS));
|
|
139
|
+
}
|
|
140
|
+
return fail(status, {
|
|
141
|
+
error: started.code === "TOO_MANY_JOBS"
|
|
142
|
+
? "Too many concurrent generation jobs"
|
|
143
|
+
: "Request ID already in use",
|
|
144
|
+
code: started.code,
|
|
145
|
+
requestId,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
jobOwned = true;
|
|
149
|
+
registerJobAbortController(requestId, cancelController);
|
|
150
|
+
if (asyncMode) {
|
|
151
|
+
res.status(202).json({ requestId, async: true });
|
|
152
|
+
}
|
|
153
|
+
setJobPhase(requestId, "streaming");
|
|
154
|
+
if (asyncMode)
|
|
155
|
+
publish(requestId, "phase", { requestId, phase: "streaming" });
|
|
156
|
+
const client = req.get("x-ima2-client") || "ui";
|
|
157
|
+
const referenceDiagnostics = refCheck.referenceDiagnostics || [];
|
|
158
|
+
const referenceMismatchCount = referenceDiagnostics.filter((ref) => ref.warnings?.includes("mime_mismatch")).length;
|
|
159
|
+
logEvent("generate", "request", {
|
|
160
|
+
requestId,
|
|
161
|
+
client,
|
|
162
|
+
provider: activeProvider,
|
|
163
|
+
quality,
|
|
164
|
+
model: imageModel,
|
|
165
|
+
size: effectiveSize,
|
|
166
|
+
moderation,
|
|
167
|
+
n: count,
|
|
168
|
+
refs: providerRefCount,
|
|
169
|
+
referenceBytes: referencePayload.referenceBytes,
|
|
170
|
+
referenceMismatchCount,
|
|
171
|
+
refDetectedMimes: [...new Set(referenceDiagnostics.map((ref) => ref.detectedMime).filter(Boolean))].join(","),
|
|
172
|
+
refDeclaredMimes: [...new Set(referenceDiagnostics.map((ref) => ref.declaredMime).filter(Boolean))].join(","),
|
|
173
|
+
sessionId,
|
|
174
|
+
clientNodeId,
|
|
175
|
+
promptChars: typeof prompt === "string" ? prompt.length : 0,
|
|
176
|
+
promptMode: normalizedPromptMode,
|
|
177
|
+
webSearchEnabled,
|
|
178
|
+
});
|
|
179
|
+
const startTime = Date.now();
|
|
180
|
+
const mimeMap = { png: "image/png", jpeg: "image/jpeg", webp: "image/webp" };
|
|
181
|
+
const effectiveFormat = activeProvider === "grok" || activeProvider === "agy" || activeProvider === "grok-api" || activeProvider === "gemini-api" ? "jpeg" : String(format);
|
|
182
|
+
const mime = mimeMap[effectiveFormat] || "image/png";
|
|
183
|
+
await mkdir(ctx.config.storage.generatedDir, { recursive: true });
|
|
184
|
+
const grokDirectApiKey = activeProvider === "grok-api" ? ctx.xaiApiKey : undefined;
|
|
185
|
+
const sharedGrokPlan = activeProvider === "grok" || activeProvider === "grok-api"
|
|
186
|
+
? await planGrokImage(generationPrompt, ctx, {
|
|
187
|
+
model: quality === "high" ? "grok-imagine-image-quality" : imageModel,
|
|
188
|
+
size: effectiveSize,
|
|
189
|
+
signal: cancelController.signal,
|
|
190
|
+
requestId,
|
|
191
|
+
referenceCount: grokRefs.length,
|
|
192
|
+
references: grokRefs,
|
|
193
|
+
directApiKey: grokDirectApiKey,
|
|
194
|
+
})
|
|
195
|
+
: null;
|
|
196
|
+
const generateOne = async () => {
|
|
197
|
+
if (activeProvider === "gemini-api") {
|
|
198
|
+
const r = await generateViaGeminiApi(generationPrompt, requireRuntimeContext(ctx), {
|
|
199
|
+
model: imageModel,
|
|
200
|
+
size: effectiveSize,
|
|
201
|
+
signal: cancelController.signal,
|
|
202
|
+
requestId,
|
|
203
|
+
references: refCheck.refDetails,
|
|
204
|
+
});
|
|
205
|
+
throwIfJobCanceled(requestId);
|
|
206
|
+
return r;
|
|
207
|
+
}
|
|
208
|
+
if (activeProvider === "agy") {
|
|
209
|
+
const r = await generateViaAgy(generationPrompt, {
|
|
210
|
+
references: refCheck.refDetails,
|
|
211
|
+
signal: cancelController.signal,
|
|
212
|
+
requestId,
|
|
213
|
+
});
|
|
214
|
+
throwIfJobCanceled(requestId);
|
|
215
|
+
return r;
|
|
216
|
+
}
|
|
217
|
+
if (activeProvider === "grok" || activeProvider === "grok-api") {
|
|
218
|
+
const grokModel = quality === "high" ? "grok-imagine-image-quality" : imageModel;
|
|
219
|
+
const r = await generateViaGrok(generationPrompt, ctx, {
|
|
220
|
+
model: grokModel,
|
|
221
|
+
size: effectiveSize,
|
|
222
|
+
signal: cancelController.signal,
|
|
223
|
+
requestId,
|
|
224
|
+
plannedPrompt: sharedGrokPlan?.prompt,
|
|
225
|
+
webSearchCalls: sharedGrokPlan?.webSearchCalls,
|
|
226
|
+
references: grokRefs,
|
|
227
|
+
directApiKey: grokDirectApiKey,
|
|
228
|
+
});
|
|
229
|
+
throwIfJobCanceled(requestId);
|
|
230
|
+
return r;
|
|
231
|
+
}
|
|
232
|
+
const MAX_RETRIES = 1;
|
|
233
|
+
let lastErr;
|
|
234
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
235
|
+
try {
|
|
236
|
+
const r = await generateViaResponses(activeProvider, generationPrompt, quality, effectiveSize, moderation, refCheck.refDetails || refCheck.refs, requestId, normalizedPromptMode, ctx, {
|
|
237
|
+
model: imageModel,
|
|
238
|
+
reasoningEffort,
|
|
239
|
+
webSearchEnabled,
|
|
240
|
+
signal: cancelController.signal,
|
|
241
|
+
allowPromptOnlyOAuthFallback: activeProvider !== "api",
|
|
242
|
+
});
|
|
243
|
+
throwIfJobCanceled(requestId);
|
|
244
|
+
if (r.b64)
|
|
245
|
+
return r;
|
|
246
|
+
lastErr = new Error("Empty response (safety refusal)");
|
|
247
|
+
}
|
|
248
|
+
catch (e) {
|
|
249
|
+
lastErr = e;
|
|
250
|
+
if (isNonRetryableGenerationError(e))
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
if (attempt < MAX_RETRIES) {
|
|
254
|
+
const errCode = (lastErr && typeof lastErr === "object" && "code" in lastErr)
|
|
255
|
+
? lastErr.code
|
|
256
|
+
: undefined;
|
|
257
|
+
logEvent("generate", "retry", { requestId, attempt: attempt + 1, errorCode: errCode });
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
throw normalizeGenerationFailure(lastErr, {
|
|
261
|
+
safetyMessage: "Content generation refused after retries",
|
|
262
|
+
});
|
|
263
|
+
};
|
|
264
|
+
const results = await Promise.allSettled(Array.from({ length: count }, generateOne));
|
|
265
|
+
throwIfJobCanceled(requestId);
|
|
266
|
+
const images = [];
|
|
267
|
+
let totalUsage = null;
|
|
268
|
+
let totalWebSearchCalls = 0;
|
|
269
|
+
let firstRetryMeta = null;
|
|
270
|
+
for (const r of results) {
|
|
271
|
+
if (r.status === "fulfilled" && r.value.b64) {
|
|
272
|
+
throwIfJobCanceled(requestId);
|
|
273
|
+
const valueWithMime = r.value;
|
|
274
|
+
const resultMime = activeProvider === "grok" || activeProvider === "agy" || activeProvider === "grok-api" || activeProvider === "gemini-api"
|
|
275
|
+
? (valueWithMime.mime || detectImageMimeFromB64(r.value.b64) || mime)
|
|
276
|
+
: mime;
|
|
277
|
+
const resultFormat = activeProvider === "grok" || activeProvider === "agy" || activeProvider === "grok-api" || activeProvider === "gemini-api" ? imageFormatFromMime(resultMime) : effectiveFormat;
|
|
278
|
+
const retryValue = r.value;
|
|
279
|
+
if (!firstRetryMeta && retryValue.retryKind) {
|
|
280
|
+
firstRetryMeta = {
|
|
281
|
+
retryKind: retryValue.retryKind,
|
|
282
|
+
initialEventCount: retryValue.initialEventCount ?? null,
|
|
283
|
+
initialEventTypes: retryValue.initialEventTypes || null,
|
|
284
|
+
referencesDroppedOnRetry: retryValue.referencesDroppedOnRetry ?? null,
|
|
285
|
+
developerPromptDroppedOnRetry: retryValue.developerPromptDroppedOnRetry ?? null,
|
|
286
|
+
webSearchDroppedOnRetry: retryValue.webSearchDroppedOnRetry ?? null,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
const rand = randomBytes(ctx.config.ids.generatedHexBytes).toString("hex");
|
|
290
|
+
const filename = `${Date.now()}_${rand}_${images.length}.${resultFormat}`;
|
|
291
|
+
const createdAt = Date.now();
|
|
292
|
+
const valueWithProviderUrl = r.value;
|
|
293
|
+
const providerUrl = typeof valueWithProviderUrl.providerUrl === "string"
|
|
294
|
+
? valueWithProviderUrl.providerUrl
|
|
295
|
+
: undefined;
|
|
296
|
+
const meta = {
|
|
297
|
+
kind: "classic",
|
|
298
|
+
requestId,
|
|
299
|
+
sessionId,
|
|
300
|
+
clientNodeId,
|
|
301
|
+
prompt,
|
|
302
|
+
userPrompt: prompt,
|
|
303
|
+
revisedPrompt: r.value.revisedPrompt || null,
|
|
304
|
+
promptMode: normalizedPromptMode,
|
|
305
|
+
composerPrompt,
|
|
306
|
+
composerInsertedPrompts,
|
|
307
|
+
quality,
|
|
308
|
+
size: effectiveSize,
|
|
309
|
+
format: resultFormat,
|
|
310
|
+
moderation,
|
|
311
|
+
model: activeProvider === "grok" ? (quality === "high" ? "grok-imagine-image-quality" : imageModel) : imageModel,
|
|
312
|
+
reasoningEffort,
|
|
313
|
+
provider: activeProvider,
|
|
314
|
+
createdAt,
|
|
315
|
+
...(providerUrl ? { providerUrl } : {}),
|
|
316
|
+
usage: r.value.usage || null,
|
|
317
|
+
webSearchCalls: r.value.webSearchCalls || 0,
|
|
318
|
+
webSearchEnabled,
|
|
319
|
+
refsCount: providerRefCount,
|
|
320
|
+
};
|
|
321
|
+
const rawBuffer = Buffer.from(r.value.b64, "base64");
|
|
322
|
+
const embedded = await embedImageMetadataBestEffort(rawBuffer, resultFormat, meta, {
|
|
323
|
+
version: ctx.packageVersion,
|
|
324
|
+
});
|
|
325
|
+
if (!embedded.embedded) {
|
|
326
|
+
logEvent("generate", "metadata_embed_skipped", { requestId, filename, code: embedded.code, warning: embedded.warning, });
|
|
327
|
+
}
|
|
328
|
+
const filePath = join(ctx.config.storage.generatedDir, filename);
|
|
329
|
+
await writeFile(filePath, embedded.buffer);
|
|
330
|
+
await safeWriteSidecar(filePath + ".json", meta);
|
|
331
|
+
generateImageThumbnailFromBuffer(embedded.buffer, filePath).catch(() => { });
|
|
332
|
+
invalidateHistoryIndex();
|
|
333
|
+
images.push({
|
|
334
|
+
image: `data:${resultMime};base64,${r.value.b64}`,
|
|
335
|
+
filename,
|
|
336
|
+
revisedPrompt: r.value.revisedPrompt || null,
|
|
337
|
+
...(providerUrl ? { providerUrl } : {}),
|
|
338
|
+
createdAt,
|
|
339
|
+
});
|
|
340
|
+
if (r.value.usage) {
|
|
341
|
+
const usageValue = r.value.usage;
|
|
342
|
+
if (!totalUsage)
|
|
343
|
+
totalUsage = { ...usageValue };
|
|
344
|
+
else {
|
|
345
|
+
const tu = totalUsage;
|
|
346
|
+
Object.keys(usageValue).forEach((k) => {
|
|
347
|
+
if (typeof usageValue[k] === "number")
|
|
348
|
+
tu[k] = (tu[k] || 0) + usageValue[k];
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
if (typeof r.value.webSearchCalls === "number") {
|
|
353
|
+
totalWebSearchCalls = activeProvider === "grok" || activeProvider === "grok-api"
|
|
354
|
+
? Math.max(totalWebSearchCalls, r.value.webSearchCalls)
|
|
355
|
+
: totalWebSearchCalls + r.value.webSearchCalls;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
else if (r.status === "rejected") {
|
|
359
|
+
logError("generate", "parallel_failed", r.reason, { requestId });
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
if (images.length === 0) {
|
|
363
|
+
const firstErr = results.find((r) => r.status === "rejected")?.reason;
|
|
364
|
+
if (firstErr?.code) {
|
|
365
|
+
const status = firstErr.status || 500;
|
|
366
|
+
if (isGenerationCanceledError(firstErr)) {
|
|
367
|
+
finishCanceled = true;
|
|
368
|
+
return fail(firstErr.status, {
|
|
369
|
+
error: firstErr.message,
|
|
370
|
+
code: firstErr.code,
|
|
371
|
+
requestId,
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
return fail(status, {
|
|
375
|
+
error: firstErr.message,
|
|
376
|
+
code: firstErr.code,
|
|
377
|
+
...upstreamErrorFields(firstErr),
|
|
378
|
+
requestId,
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
return fail(500, { error: "All generation attempts failed", code: "GENERATE_ALL_FAILED", requestId });
|
|
382
|
+
}
|
|
383
|
+
const elapsed = +((Date.now() - startTime) / 1000).toFixed(1);
|
|
384
|
+
// Patch elapsed into sidecars after the generation loop (best effort).
|
|
385
|
+
await Promise.all(images.map(async ({ filename }) => {
|
|
386
|
+
try {
|
|
387
|
+
const sidecarPath = join(ctx.config.storage.generatedDir, filename + ".json");
|
|
388
|
+
const sidecarMeta = JSON.parse(await readFile(sidecarPath, "utf-8"));
|
|
389
|
+
sidecarMeta.elapsed = elapsed;
|
|
390
|
+
await atomicWriteJson(sidecarPath, sidecarMeta);
|
|
391
|
+
}
|
|
392
|
+
catch {
|
|
393
|
+
/* best-effort elapsed patch */
|
|
394
|
+
}
|
|
395
|
+
}));
|
|
396
|
+
const firstRevised = images[0]?.revisedPrompt || null;
|
|
397
|
+
const extra = {
|
|
398
|
+
usage: totalUsage,
|
|
399
|
+
provider: activeProvider,
|
|
400
|
+
reasoningEffort,
|
|
401
|
+
webSearchCalls: totalWebSearchCalls,
|
|
402
|
+
quality,
|
|
403
|
+
size: effectiveSize,
|
|
404
|
+
moderation,
|
|
405
|
+
model: imageModel,
|
|
406
|
+
warnings: qualityWarnings,
|
|
407
|
+
revisedPrompt: firstRevised,
|
|
408
|
+
promptMode: normalizedPromptMode,
|
|
409
|
+
webSearchEnabled,
|
|
410
|
+
...(firstRetryMeta || {}),
|
|
411
|
+
};
|
|
412
|
+
if (count === 1) {
|
|
413
|
+
finishHttpStatus = 200;
|
|
414
|
+
finishMeta = { filenames: [images[0].filename], imageCount: 1 };
|
|
415
|
+
logEvent("generate", "saved", { requestId, imageCount: 1, elapsedMs: Date.now() - startTime, filename: images[0].filename, });
|
|
416
|
+
succeed({
|
|
417
|
+
image: images[0].image,
|
|
418
|
+
elapsed,
|
|
419
|
+
filename: images[0].filename,
|
|
420
|
+
requestId,
|
|
421
|
+
providerUrl: images[0].providerUrl ?? null,
|
|
422
|
+
createdAt: images[0].createdAt,
|
|
423
|
+
...extra,
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
else {
|
|
427
|
+
finishHttpStatus = 200;
|
|
428
|
+
finishMeta = { filenames: images.map((image) => image.filename), imageCount: images.length };
|
|
429
|
+
logEvent("generate", "saved", { requestId, imageCount: images.length, elapsedMs: Date.now() - startTime, });
|
|
430
|
+
succeed({ images, elapsed, count: images.length, requestId, ...extra });
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
catch (e) {
|
|
434
|
+
const err = errInfo(e);
|
|
435
|
+
const ext = (err.raw && typeof err.raw === "object" ? err.raw : {});
|
|
436
|
+
const fallbackCode = err.code || classifyUpstreamError(err.message);
|
|
437
|
+
if (isGenerationCanceledError(err.raw) || isJobCanceled(requestId)) {
|
|
438
|
+
const canceled = makeGenerationCanceledError();
|
|
439
|
+
finishCanceled = true;
|
|
440
|
+
return fail(canceled.status, {
|
|
441
|
+
error: canceled.message,
|
|
442
|
+
code: canceled.code,
|
|
443
|
+
requestId,
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
finishErrorCode = fallbackCode || "GENERATE_FAILED";
|
|
447
|
+
logError("generate", "error", err.raw, { requestId, code: finishErrorCode });
|
|
448
|
+
fail(err.status || 500, {
|
|
449
|
+
error: err.message,
|
|
450
|
+
code: finishErrorCode,
|
|
451
|
+
...upstreamErrorFields(ext),
|
|
452
|
+
requestId,
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
finally {
|
|
456
|
+
if (jobOwned)
|
|
457
|
+
finishJob(requestId, {
|
|
458
|
+
canceled: finishCanceled,
|
|
459
|
+
status: finishStatus,
|
|
460
|
+
httpStatus: finishHttpStatus,
|
|
461
|
+
errorCode: finishErrorCode,
|
|
462
|
+
meta: finishMeta,
|
|
463
|
+
});
|
|
464
|
+
appendGenerationRequestLog(ctx.config.storage.generationRequestLogFile, {
|
|
465
|
+
id: randomBytes(8).toString("hex"),
|
|
466
|
+
requestId,
|
|
467
|
+
createdAt: Date.now(),
|
|
468
|
+
prompt: typeof req.body?.prompt === "string" ? req.body.prompt : "",
|
|
469
|
+
requested: parseInt(req.body?.n) || 1,
|
|
470
|
+
succeeded: finishStatus === "completed" ? (finishMeta.imageCount ?? 1) : 0,
|
|
471
|
+
error: finishStatus === "error" ? (finishErrorCode ?? "unknown") : null,
|
|
472
|
+
}).catch(() => { });
|
|
473
|
+
}
|
|
474
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { normalizeRequestId } from "./requestLogger.js";
|
|
2
|
+
export const MAX_GENERATION_PROMPT_CHARS = 32_000;
|
|
3
|
+
export function normalizeBodyRequestId(bodyRequestId, fallbackRequestId) {
|
|
4
|
+
return normalizeRequestId(bodyRequestId === undefined ? fallbackRequestId : bodyRequestId);
|
|
5
|
+
}
|
|
6
|
+
export function validateGenerationPrompt(prompt) {
|
|
7
|
+
if (typeof prompt !== "string" || prompt.length === 0) {
|
|
8
|
+
return { error: { code: "INVALID_PROMPT", message: "Prompt must be a non-empty string" } };
|
|
9
|
+
}
|
|
10
|
+
if (prompt.length > MAX_GENERATION_PROMPT_CHARS) {
|
|
11
|
+
return {
|
|
12
|
+
error: {
|
|
13
|
+
code: "PROMPT_TOO_LONG",
|
|
14
|
+
message: `Prompt must not exceed ${MAX_GENERATION_PROMPT_CHARS} characters`,
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
export function validateBoundedCount(value, fallback, maximum, field) {
|
|
21
|
+
const candidate = value === undefined ? fallback : value;
|
|
22
|
+
const parsed = typeof candidate === "number" ? candidate : Number(candidate);
|
|
23
|
+
if (!Number.isInteger(parsed) || parsed < 1 || parsed > maximum) {
|
|
24
|
+
return {
|
|
25
|
+
error: {
|
|
26
|
+
code: "INVALID_COUNT",
|
|
27
|
+
message: `${field} must be an integer between 1 and ${maximum}`,
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
return { value: parsed };
|
|
32
|
+
}
|
package/lib/grokImageCore.js
CHANGED
|
@@ -88,7 +88,24 @@ export async function downloadGrokImageUrl(url, signal, timeoutMs = 30_000) {
|
|
|
88
88
|
if (contentLength > MAX_IMAGE_DOWNLOAD_BYTES) {
|
|
89
89
|
throw grokError("Image download exceeds 50MB limit", 502, "GROK_IMAGE_DOWNLOAD_FAILED");
|
|
90
90
|
}
|
|
91
|
-
|
|
91
|
+
if (!res.body)
|
|
92
|
+
throw grokError("Image download had no response body", 502, "GROK_IMAGE_DOWNLOAD_FAILED");
|
|
93
|
+
const chunks = [];
|
|
94
|
+
let total = 0;
|
|
95
|
+
const reader = res.body.getReader();
|
|
96
|
+
while (true) {
|
|
97
|
+
const { done, value } = await reader.read();
|
|
98
|
+
if (done)
|
|
99
|
+
break;
|
|
100
|
+
total += value.byteLength;
|
|
101
|
+
if (total > MAX_IMAGE_DOWNLOAD_BYTES) {
|
|
102
|
+
await reader.cancel("download size limit exceeded").catch(() => { });
|
|
103
|
+
controller.abort();
|
|
104
|
+
throw grokError("Image download exceeds 50MB limit", 502, "GROK_IMAGE_DOWNLOAD_FAILED");
|
|
105
|
+
}
|
|
106
|
+
chunks.push(Buffer.from(value));
|
|
107
|
+
}
|
|
108
|
+
const buffer = Buffer.concat(chunks, total);
|
|
92
109
|
clearTimeout(timer);
|
|
93
110
|
if (buffer.length === 0)
|
|
94
111
|
throw grokError("Image download was empty", 502, "GROK_IMAGE_DOWNLOAD_FAILED");
|
package/lib/historyIndex.js
CHANGED
|
@@ -3,6 +3,7 @@ import { listHistoryRows } from "./historyList.js";
|
|
|
3
3
|
const HISTORY_INDEX_TTL_MS = 3000;
|
|
4
4
|
let snapshot = null;
|
|
5
5
|
let pending = null;
|
|
6
|
+
let generation = 0;
|
|
6
7
|
function isFreshIndex(current, baseDir) {
|
|
7
8
|
return Boolean(current &&
|
|
8
9
|
current.baseDir === baseDir &&
|
|
@@ -13,17 +14,23 @@ export async function getHistoryIndex(baseDir = config.storage.generatedDir) {
|
|
|
13
14
|
return snapshot;
|
|
14
15
|
if (pending)
|
|
15
16
|
return pending;
|
|
16
|
-
|
|
17
|
+
const scanGeneration = generation;
|
|
18
|
+
const scan = (async () => {
|
|
17
19
|
const rows = await listHistoryRows(baseDir);
|
|
18
20
|
const next = { baseDir, builtAt: Date.now(), rows };
|
|
19
|
-
|
|
21
|
+
if (scanGeneration === generation)
|
|
22
|
+
snapshot = next;
|
|
20
23
|
return next;
|
|
21
|
-
})()
|
|
22
|
-
|
|
24
|
+
})();
|
|
25
|
+
const tracked = scan.finally(() => {
|
|
26
|
+
if (pending === tracked)
|
|
27
|
+
pending = null;
|
|
23
28
|
});
|
|
29
|
+
pending = tracked;
|
|
24
30
|
return pending;
|
|
25
31
|
}
|
|
26
32
|
export function invalidateHistoryIndex() {
|
|
33
|
+
generation += 1;
|
|
27
34
|
snapshot = null;
|
|
28
35
|
pending = null;
|
|
29
36
|
}
|
package/lib/historyList.js
CHANGED
|
@@ -79,6 +79,9 @@ export async function listHistoryRows(baseDir = config.storage.generatedDir) {
|
|
|
79
79
|
canvasSourceFilename: meta?.canvasSourceFilename || null,
|
|
80
80
|
canvasEditableFilename: meta?.canvasEditableFilename || null,
|
|
81
81
|
canvasMergedAt: Number.isFinite(meta?.canvasMergedAt) ? meta.canvasMergedAt : null,
|
|
82
|
+
annotationsBaked: Boolean(meta?.annotationsBaked),
|
|
83
|
+
annotationSnapshot: meta?.annotationSnapshot ?? null,
|
|
84
|
+
annotationOnly: Boolean(meta?.annotationOnly),
|
|
82
85
|
setId: meta?.setId || null,
|
|
83
86
|
cardId: meta?.cardId || null,
|
|
84
87
|
cardOrder: Number.isFinite(meta?.cardOrder) ? meta.cardOrder : null,
|
package/lib/imageMetadata.js
CHANGED
|
@@ -38,6 +38,9 @@ export function buildIma2MetadataPayload(meta = {}, context = {}) {
|
|
|
38
38
|
canvasSourceFilename: stringOrNull(meta.canvasSourceFilename, 240),
|
|
39
39
|
canvasEditableFilename: stringOrNull(meta.canvasEditableFilename, 240),
|
|
40
40
|
canvasMergedAt: numberOrNull(meta.canvasMergedAt),
|
|
41
|
+
annotationsBaked: Boolean(meta.annotationsBaked),
|
|
42
|
+
annotationSnapshot: isPlainObject(meta.annotationSnapshot) ? meta.annotationSnapshot : null,
|
|
43
|
+
annotationOnly: Boolean(meta.annotationOnly),
|
|
41
44
|
prompt: stringOrNull(meta.prompt),
|
|
42
45
|
userPrompt: stringOrNull(meta.userPrompt) || stringOrNull(meta.prompt),
|
|
43
46
|
revisedPrompt: stringOrNull(meta.revisedPrompt),
|