ima2-gen 2.0.2 → 2.0.3
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/CHANGELOG.md +1 -0
- package/bin/commands/gen.js +7 -5
- package/bin/commands/multimode.js +7 -5
- package/bin/commands/node.js +2 -1
- package/config.js +8 -1
- package/docs/API.md +5 -4
- package/docs/CLI.md +4 -4
- package/docs/FAQ.ko.md +1 -1
- package/docs/FAQ.md +1 -1
- package/docs/migration/runtime-test-inventory.md +11 -1
- package/lib/agentCommandParser.js +10 -6
- package/lib/agentGenerationPlanner.js +96 -19
- package/lib/agentImageVideoGen.js +26 -6
- package/lib/agentPlannerModel.js +172 -0
- package/lib/agentQuestionResponder.js +9 -3
- package/lib/agentQueueStore.js +42 -0
- package/lib/agentQueueWorker.js +57 -7
- package/lib/agentRuntime.js +96 -12
- package/lib/agentSettings.js +9 -6
- package/lib/agentToolManifest.js +90 -0
- package/lib/agentTypes.js +1 -0
- package/lib/agyCli.js +31 -0
- package/lib/agyImageAdapter.js +73 -8
- package/lib/capabilities.js +7 -5
- package/lib/configKeys.js +6 -0
- package/lib/geminiApiImageAdapter.js +11 -4
- package/lib/generationRequestLog.js +25 -0
- package/lib/grokImageAdapter.js +27 -34
- package/lib/grokMultimodeAdapter.js +2 -1
- package/lib/grokVideoAdapter.js +3 -2
- package/lib/grokVideoPlannerPrompt.js +18 -25
- package/lib/inflight.js +1 -1
- package/lib/multimodeHelpers.js +3 -2
- package/lib/oauthProxy/generators.js +6 -5
- package/lib/oauthProxy/prompts.js +41 -16
- package/lib/oauthProxy/streams.js +1 -1
- package/lib/promptSafetyPolicy.js +1 -1
- package/lib/responsesDoctor.js +3 -3
- package/lib/responsesFallback.js +27 -14
- package/lib/responsesImageAdapter.js +11 -7
- package/node_modules/qs/CHANGELOG.md +10 -0
- package/node_modules/qs/README.md +1 -1
- package/node_modules/qs/dist/qs.js +15 -15
- package/node_modules/qs/eslint.config.mjs +1 -0
- package/node_modules/qs/lib/parse.js +52 -22
- package/node_modules/qs/lib/stringify.js +11 -4
- package/node_modules/qs/package.json +2 -2
- package/node_modules/qs/test/parse.js +49 -0
- package/node_modules/qs/test/stringify.js +129 -0
- package/package.json +1 -1
- package/routes/agent.js +19 -2
- package/routes/agy.js +5 -1
- package/routes/generate.js +12 -1
- package/routes/generationRequestLog.js +16 -0
- package/routes/index.js +2 -0
- package/routes/keys.js +22 -2
- package/routes/multimode.js +1 -1
- package/routes/nodes.js +5 -5
- package/ui/dist/.vite/manifest.json +22 -12
- package/ui/dist/assets/AgentWorkspace-qEMrATBP.js +3 -0
- package/ui/dist/assets/{CardNewsWorkspace-Dav3K5CT.js → CardNewsWorkspace-DoJnVHmn.js} +1 -1
- package/ui/dist/assets/GenerationRequestLogPanel-BDWRRLNV.js +1 -0
- package/ui/dist/assets/{NodeCanvas-C4ifFzB1.js → NodeCanvas-DIMo45sp.js} +3 -3
- package/ui/dist/assets/{PromptBuilderPanel-CEcyU9PL.js → PromptBuilderPanel-BF3lcSJD.js} +2 -2
- package/ui/dist/assets/{PromptImportDialog-CgQ94Gth.js → PromptImportDialog-Cy6ZymKG.js} +2 -2
- package/ui/dist/assets/{PromptImportDiscoverySection-CuzyzbNI.js → PromptImportDiscoverySection-CzuW-8P2.js} +1 -1
- package/ui/dist/assets/PromptImportFolderSection-DP5ywnsv.js +1 -0
- package/ui/dist/assets/{PromptLibraryPanel-BOe18we8.js → PromptLibraryPanel-CAsKr7CV.js} +2 -2
- package/ui/dist/assets/SettingsWorkspace-_PdPVsxi.js +1 -0
- package/ui/dist/assets/index-BUinlX2n.js +4 -0
- package/ui/dist/assets/index-CX3fge8X.css +1 -0
- package/ui/dist/assets/index-ygo6nfqx.js +23 -0
- package/ui/dist/index.html +2 -2
- package/ui/dist/assets/AgentWorkspace-Dth6YijN.js +0 -3
- package/ui/dist/assets/PromptImportFolderSection-DHLGlO6l.js +0 -1
- package/ui/dist/assets/SettingsWorkspace-Cdgnm4Wa.js +0 -1
- package/ui/dist/assets/index-C5PSahkr.js +0 -1
- package/ui/dist/assets/index-Dn2AhL6d.css +0 -1
- package/ui/dist/assets/index-Tjqx6wUV.js +0 -23
package/CHANGELOG.md
CHANGED
|
@@ -30,6 +30,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
30
30
|
- Node route validation order — `startJob`/202 response moved after request validation.
|
|
31
31
|
- CI typecheck — unused imports in card-news tests and store split type mismatches.
|
|
32
32
|
- Thumbnail backfill failure reporting (#94).
|
|
33
|
+
- AGY provider detection now finds user-local `agy` installs such as `~/.local/bin/agy` and supports `IMA2_AGY_BIN` for explicit binary paths.
|
|
33
34
|
- AGY Windows pipe handling, Gemini API aspect ratio string values, multimode same-prompt batching.
|
|
34
35
|
- Moderation over-filtering — removed safety tags and added error enrichment.
|
|
35
36
|
|
package/bin/commands/gen.js
CHANGED
|
@@ -9,6 +9,8 @@ const VALID_MODES = new Set(["auto", "direct"]);
|
|
|
9
9
|
const VALID_MODERATION = new Set(["auto", "low"]);
|
|
10
10
|
const VALID_PROVIDERS = new Set(["auto", "oauth", "api", "grok", "grok-api", "agy", "gemini-api"]);
|
|
11
11
|
const KNOWN_IMAGE_MODELS = new Set(["gpt-5.5", "gpt-5.4", "gpt-5.4-mini", "gpt-5.3-codex-spark", "grok-imagine-image", "grok-imagine-image-quality", "nano-banana-2", "nano-banana-pro"]);
|
|
12
|
+
const MAX_GENERATION_COUNT = Math.max(1, Math.trunc(Number(config.limits.maxGeneratedImages) || 24));
|
|
13
|
+
const MAX_REFERENCE_COUNT = Math.max(1, Math.trunc(Number(config.limits.maxRefCount) || 5));
|
|
12
14
|
const SPEC = {
|
|
13
15
|
flags: {
|
|
14
16
|
quality: { short: "q", type: "string", default: "low" },
|
|
@@ -47,8 +49,8 @@ const HELP = `
|
|
|
47
49
|
Options:
|
|
48
50
|
-q, --quality <low|medium|high> Default: low
|
|
49
51
|
-s, --size <WxH | auto> Default: 1024x1024
|
|
50
|
-
-n, --count <1
|
|
51
|
-
--ref <file> Attach reference image (repeatable, max
|
|
52
|
+
-n, --count <1..${MAX_GENERATION_COUNT}> Default: 1
|
|
53
|
+
--ref <file> Attach reference image (repeatable, max ${MAX_REFERENCE_COUNT})
|
|
52
54
|
-o, --out <file> Single-image output path (implies -n 1)
|
|
53
55
|
-d, --out-dir <dir> Output dir for multiple images
|
|
54
56
|
--json Print JSON result to stdout
|
|
@@ -88,8 +90,8 @@ export default async function genCmd(argv) {
|
|
|
88
90
|
if (!prompt)
|
|
89
91
|
die(2, "prompt is required (positional or via --stdin)");
|
|
90
92
|
const refs = (Array.isArray(args.ref) ? args.ref : []);
|
|
91
|
-
if (refs.length >
|
|
92
|
-
die(2,
|
|
93
|
+
if (refs.length > MAX_REFERENCE_COUNT)
|
|
94
|
+
die(2, `max ${MAX_REFERENCE_COUNT} --ref attachments`);
|
|
93
95
|
if (!VALID_MODES.has(String(args.mode)))
|
|
94
96
|
die(2, "--mode must be one of: auto, direct");
|
|
95
97
|
if (!VALID_MODERATION.has(String(args.moderation)))
|
|
@@ -107,7 +109,7 @@ export default async function genCmd(argv) {
|
|
|
107
109
|
if (args["web-search"] && args["no-web-search"]) {
|
|
108
110
|
die(2, "--web-search and --no-web-search are mutually exclusive");
|
|
109
111
|
}
|
|
110
|
-
const n = Math.max(1, Math.min(
|
|
112
|
+
const n = Math.max(1, Math.min(MAX_GENERATION_COUNT, parseInt(String(args.count)) || 1));
|
|
111
113
|
const timeoutMs = (parseInt(String(args.timeout)) || 180) * 1000;
|
|
112
114
|
let server;
|
|
113
115
|
try {
|
|
@@ -5,6 +5,8 @@ import { dataUriToFile, defaultOutName, fileToDataUri } from "../lib/files.js";
|
|
|
5
5
|
import { out, die, color, json, exitCodeForError } from "../lib/output.js";
|
|
6
6
|
import { config } from "../../config.js";
|
|
7
7
|
import { createCliRequestId, recoverGeneratedOutputs, formatRecoveryHint } from "../lib/recover-output.js";
|
|
8
|
+
const MAX_GENERATION_COUNT = Math.max(1, Math.trunc(Number(config.limits.maxGeneratedImages) || 24));
|
|
9
|
+
const MAX_REFERENCE_COUNT = Math.max(1, Math.trunc(Number(config.limits.maxRefCount) || 5));
|
|
8
10
|
const SPEC = {
|
|
9
11
|
flags: {
|
|
10
12
|
quality: { short: "q", type: "string", default: "low" },
|
|
@@ -36,7 +38,7 @@ const HELP = `
|
|
|
36
38
|
Options:
|
|
37
39
|
-q, --quality <low|medium|high> Default: low
|
|
38
40
|
-s, --size <WxH> Default: 1024x1024
|
|
39
|
-
--max-images <1
|
|
41
|
+
--max-images <1..${MAX_GENERATION_COUNT}> Default: 4
|
|
40
42
|
-o, --out <file> First image (implies --max-images 1)
|
|
41
43
|
-d, --out-dir <dir> Output dir for multiple images
|
|
42
44
|
--json
|
|
@@ -44,7 +46,7 @@ const HELP = `
|
|
|
44
46
|
--provider <auto|oauth|api|grok|grok-api|agy|gemini-api>
|
|
45
47
|
Provider (oauth = GPT OAuth; grok = xAI Grok; agy/gemini-api = Gemini)
|
|
46
48
|
--mode <auto|direct> Prompt handling mode. Default: auto
|
|
47
|
-
--ref <file> Attach reference image (repeatable, max
|
|
49
|
+
--ref <file> Attach reference image (repeatable, max ${MAX_REFERENCE_COUNT})
|
|
48
50
|
--reasoning-effort <none|low|medium|high|xhigh>
|
|
49
51
|
--web-search / --no-web-search
|
|
50
52
|
--moderation <auto|low>
|
|
@@ -83,10 +85,10 @@ export default async function multimodeCmd(argv) {
|
|
|
83
85
|
die(exitCodeForError(e), e.message);
|
|
84
86
|
throw e;
|
|
85
87
|
}
|
|
86
|
-
const maxImages = Math.max(1, Math.min(
|
|
88
|
+
const maxImages = Math.max(1, Math.min(MAX_GENERATION_COUNT, parseInt(String(args["max-images"])) || 4));
|
|
87
89
|
const refs = (Array.isArray(args.ref) ? args.ref : []);
|
|
88
|
-
if (refs.length >
|
|
89
|
-
die(2,
|
|
90
|
+
if (refs.length > MAX_REFERENCE_COUNT)
|
|
91
|
+
die(2, `max ${MAX_REFERENCE_COUNT} --ref attachments`);
|
|
90
92
|
const references = await Promise.all(refs.map((p) => fileToDataUri(p)));
|
|
91
93
|
const outDir = args["out-dir"] ? String(args["out-dir"]) : null;
|
|
92
94
|
const explicitOut = args.out ? String(args.out) : null;
|
package/bin/commands/node.js
CHANGED
|
@@ -4,6 +4,7 @@ import { streamSse } from "../lib/sse.js";
|
|
|
4
4
|
import { fileToDataUri, dataUriToFile, defaultOutName } from "../lib/files.js";
|
|
5
5
|
import { out, die, color, json, exitCodeForError } from "../lib/output.js";
|
|
6
6
|
import { config } from "../../config.js";
|
|
7
|
+
const MAX_GENERATION_COUNT = Math.max(1, Math.trunc(Number(config.limits.maxGeneratedImages) || 24));
|
|
7
8
|
const HELP = `
|
|
8
9
|
ima2 node <subcommand> [options]
|
|
9
10
|
|
|
@@ -75,7 +76,7 @@ async function generateSub(argv) {
|
|
|
75
76
|
prompt,
|
|
76
77
|
quality: args.quality,
|
|
77
78
|
size: args.size,
|
|
78
|
-
n: Math.max(1, Math.min(
|
|
79
|
+
n: Math.max(1, Math.min(MAX_GENERATION_COUNT, parseInt(String(args.count)) || 1)),
|
|
79
80
|
references,
|
|
80
81
|
moderation: args.moderation,
|
|
81
82
|
sessionId: args.session,
|
package/config.js
CHANGED
|
@@ -79,7 +79,8 @@ export const config = {
|
|
|
79
79
|
maxRefB64Bytes: pickInt(env.IMA2_MAX_REF_B64_BYTES, fileCfg.limits?.maxRefB64Bytes, 7 * 1024 * 1024),
|
|
80
80
|
maxMetadataReadB64Bytes: pickInt(env.IMA2_MAX_METADATA_READ_B64_BYTES, fileCfg.limits?.maxMetadataReadB64Bytes, 12 * 1024 * 1024),
|
|
81
81
|
maxRefCount: pickInt(env.IMA2_MAX_REF_COUNT, fileCfg.limits?.maxRefCount, 5),
|
|
82
|
-
|
|
82
|
+
maxGeneratedImages: pickInt(env.IMA2_MAX_GENERATED_IMAGES, fileCfg.limits?.maxGeneratedImages, 24),
|
|
83
|
+
maxParallel: pickInt(env.IMA2_MAX_PARALLEL, fileCfg.limits?.maxParallel, 24),
|
|
83
84
|
graphMaxNodes: pickInt(env.IMA2_GRAPH_MAX_NODES, fileCfg.limits?.graphMaxNodes, 500),
|
|
84
85
|
graphMaxEdges: pickInt(env.IMA2_GRAPH_MAX_EDGES, fileCfg.limits?.graphMaxEdges, 1000),
|
|
85
86
|
promptImportMaxFileBytes: pickInt(env.IMA2_PROMPT_IMPORT_MAX_FILE_BYTES, fileCfg.limits?.promptImportMaxFileBytes, 512 * 1024),
|
|
@@ -133,6 +134,7 @@ export const config = {
|
|
|
133
134
|
promptImportDiscoveryRegistryFile: pickStr(env.IMA2_PROMPT_IMPORT_DISCOVERY_REGISTRY_FILE, fileCfg.storage?.promptImportDiscoveryRegistryFile, join(configDir, "prompt-import-discovery.json")),
|
|
134
135
|
configFile: join(configDir, "config.json"),
|
|
135
136
|
advertiseFile: pickStr(env.IMA2_ADVERTISE_FILE, fileCfg.storage?.advertiseFile, join(configDir, "server.json")),
|
|
137
|
+
generationRequestLogFile: pickStr(env.IMA2_GENERATION_REQUEST_LOG_FILE, fileCfg.storage?.generationRequestLogFile, join(configDir, "generation-request-log.json")),
|
|
136
138
|
staticMaxAge: pickStr(env.IMA2_STATIC_MAX_AGE, fileCfg.storage?.staticMaxAge, "1y"),
|
|
137
139
|
},
|
|
138
140
|
ids: {
|
|
@@ -193,6 +195,10 @@ export const config = {
|
|
|
193
195
|
timeoutMs: pickInt(env.IMA2_CARD_NEWS_PLANNER_TIMEOUT_MS, fileCfg.cardNewsPlanner?.timeoutMs, 60_000),
|
|
194
196
|
deterministicFallback: pickBool(env.IMA2_CARD_NEWS_PLANNER_FALLBACK, fileCfg.cardNewsPlanner?.deterministicFallback, false),
|
|
195
197
|
},
|
|
198
|
+
agentPlanner: {
|
|
199
|
+
enabled: pickBool(env.IMA2_AGENT_PLANNER_ENABLED, fileCfg.agentPlanner?.enabled, true),
|
|
200
|
+
timeoutMs: pickInt(env.IMA2_AGENT_PLANNER_TIMEOUT_MS, fileCfg.agentPlanner?.timeoutMs, 30_000),
|
|
201
|
+
},
|
|
196
202
|
comfy: {
|
|
197
203
|
defaultUrl: pickStr(env.IMA2_COMFY_URL, fileCfg.comfy?.defaultUrl, "http://127.0.0.1:8188"),
|
|
198
204
|
uploadTimeoutMs: pickPositiveInt(env.IMA2_COMFY_UPLOAD_TIMEOUT_MS, fileCfg.comfy?.uploadTimeoutMs, 30_000),
|
|
@@ -216,6 +222,7 @@ export const GENERATED_DIR = config.storage.generatedDir;
|
|
|
216
222
|
export const BODY_LIMIT = config.server.bodyLimit;
|
|
217
223
|
export const MAX_REF_B64_BYTES = config.limits.maxRefB64Bytes;
|
|
218
224
|
export const MAX_REFS = config.limits.maxRefCount;
|
|
225
|
+
export const MAX_GENERATED_IMAGES = config.limits.maxGeneratedImages;
|
|
219
226
|
export const MAX_N = config.limits.maxParallel;
|
|
220
227
|
export const INFLIGHT_TTL_MS = config.inflight.ttlMs;
|
|
221
228
|
export const INFLIGHT_REAP_MS = config.inflight.reapMs;
|
package/docs/API.md
CHANGED
|
@@ -17,7 +17,7 @@ Image generation supports OAuth, API-key, Grok, and Gemini (`agy` and `gemini-ap
|
|
|
17
17
|
- `provider: "grok"` uses the bundled progrok xAI proxy. Classic, Node, and Agent generation run mandatory xAI Web Search through `/v1/responses`, then run a `grok-4.3` planner call with a forced local `generate_image` function, then ima2 executes xAI `/v1/images/generations`. If reference images, a Node parent image, or an Agent current image are attached, the final step switches to xAI `/v1/images/edits` so image-to-image context is preserved.
|
|
18
18
|
- `provider: "agy"` spawns the Antigravity CLI (`agy -p`) to generate images via Google Gemini's `default_api:generate_image` tool. Model is `nano-banana-2`. Output is fixed at 1024×1024 JPEG. Max 3 reference images (i2i). No web search, quality, size, or mask controls. Multimode returns a single image. Video is unsupported (`AGY_VIDEO_UNSUPPORTED`).
|
|
19
19
|
- `provider: "grok-api"` uses a direct xAI API key instead of the bundled progrok OAuth proxy. Same pipeline as `grok` (Web Search → planner → `/v1/images/generations`), same aspect ratio and resolution options. Requires an xAI API key configured via the web UI key management or `XAI_API_KEY` env var. Also supports video generation.
|
|
20
|
-
- `provider: "gemini-api"` calls the Google Generative Language API directly (or Vertex AI with a service account JSON). Supports models `nano-banana-2` (Gemini 3.1 Flash Image) and `nano-banana-pro` (Gemini 3 Pro Image). Supports variable aspect ratios (1:1 through 21:9) and four resolution tiers (512px, 1K, 2K, 4K)
|
|
20
|
+
- `provider: "gemini-api"` calls the Google Generative Language API directly (or Vertex AI with a service account JSON). Supports models `nano-banana-2` (Gemini 3.1 Flash Image) and `nano-banana-pro` (Gemini 3 Pro Image). Supports variable aspect ratios (1:1 through 21:9) and four resolution tiers (512px, 1K, 2K, 4K) on both auth paths — the direct API path sends `generation_config.response_format.image` (snake_case) while the Vertex AI endpoint (`aiplatform.googleapis.com`) sends `generationConfig.imageConfig` (camelCase). With `size: "auto"` the image config is omitted entirely and the model decides ratio/size. Auth: `GEMINI_API_KEY` env var, web UI key management (`/api/keys/gemini`), or a Vertex AI service account JSON (`VERTEX_SERVICE_ACCOUNT_JSON` or `/api/keys/vertex`). When both Vertex credentials and an API key are configured, Vertex takes priority. The chosen auth mode (`apikey` or `vertex`) persists to `~/.ima2/config.json` as `geminiAuthMode` and is restored on server startup. Per-model cost: `nano-banana-2` (Flash): 512=$0.001, 1K=$0.003, 2K=$0.004, 4K=$0.006; `nano-banana-pro`: 1K=$0.007, 2K=$0.007, 4K=$0.013. No web search or mask controls.
|
|
21
21
|
- API-key generation covers classic generate, edit, mask-guided edit, multimode, and node generation.
|
|
22
22
|
- If `provider: "api"` is requested without an API key, routes fail before upstream with `401` and `API_KEY_REQUIRED`.
|
|
23
23
|
- Grok generation maps `size` to xAI `aspect_ratio` and `resolution`; it does not send an OpenAI-style `size` field upstream. Grok edit uses xAI `/v1/images/edits`; Grok mask edit remains unsupported and returns `GROK_MASK_UNSUPPORTED`.
|
|
@@ -163,7 +163,7 @@ data: {"requestId":"req_abc","jobId":"req_abc","phase":"streaming"}
|
|
|
163
163
|
|---|---|---|
|
|
164
164
|
| Accepted | `202` | `{ "requestId": "req_xxx" }` |
|
|
165
165
|
| Duplicate active `requestId` | `409` | `REQUEST_ID_IN_USE` |
|
|
166
|
-
| More than
|
|
166
|
+
| More than the configured concurrent active job limit | `429` | `TOO_MANY_JOBS` with `Retry-After: 5`; default limit is `24` via `IMA2_MAX_PARALLEL` |
|
|
167
167
|
|
|
168
168
|
Progress events are published on `GET /api/events`. The POST response returns immediately; clients must not expect SSE on the POST connection when `async: true`.
|
|
169
169
|
|
|
@@ -604,7 +604,7 @@ Style-sheet extraction can require an API key/openai client. Image generation al
|
|
|
604
604
|
| `VIDEO_PROVIDER_UNSUPPORTED` | Video generation requires provider `"grok"` or `"grok-api"` |
|
|
605
605
|
| `SSE_CAPACITY` | More than 512 concurrent `GET /api/events` listeners |
|
|
606
606
|
| `REQUEST_ID_IN_USE` | Async POST used a `requestId` that already has an active job |
|
|
607
|
-
| `TOO_MANY_JOBS` | More than
|
|
607
|
+
| `TOO_MANY_JOBS` | More than the configured concurrent active generation job limit (`Retry-After: 5`; default `24`) |
|
|
608
608
|
|
|
609
609
|
## Key Management
|
|
610
610
|
|
|
@@ -612,11 +612,12 @@ API key management endpoints for configuring provider credentials at runtime thr
|
|
|
612
612
|
|
|
613
613
|
| Endpoint | Method | Description |
|
|
614
614
|
|---|---|---|
|
|
615
|
-
| `/api/keys/status` | GET | Returns configured/valid/maskedKey status for all providers (openai, xai, gemini, vertex) |
|
|
615
|
+
| `/api/keys/status` | GET | Returns configured/valid/maskedKey status for all providers (openai, xai, gemini, vertex) plus `geminiAuthMode` (`"apikey"` or `"vertex"`) |
|
|
616
616
|
| `/api/keys/:provider` | PUT | Save an API key. Body: `{ "apiKey": "..." }`. Validates key format and upstream before saving to config.json. Provider: `openai`, `xai`, or `gemini`. |
|
|
617
617
|
| `/api/keys/:provider` | DELETE | Remove a config-sourced API key. Env-sourced keys cannot be removed (`ENV_KEY_IMMUTABLE`). |
|
|
618
618
|
| `/api/keys/vertex` | PUT | Save a Vertex AI service account JSON. Body: `{ "serviceAccountJson": "..." }`. Validates JSON structure (`type: "service_account"`, `project_id` required). |
|
|
619
619
|
| `/api/keys/vertex` | DELETE | Remove a config-sourced Vertex AI service account. |
|
|
620
|
+
| `/api/keys/gemini-auth-mode` | PUT | Persist the Gemini auth mode chosen in the settings dropdown. Body: `{ "mode": "apikey" \| "vertex" }`. Saved to `config.json` and hot-updated. |
|
|
620
621
|
|
|
621
622
|
Keys saved via PUT are stored in `config.json` and hot-updated in the runtime context (no server restart required). Keys loaded from environment variables (`OPENAI_API_KEY`, `XAI_API_KEY`, `GEMINI_API_KEY`, `VERTEX_SERVICE_ACCOUNT_JSON`) take precedence and are immutable through the API.
|
|
622
623
|
|
package/docs/CLI.md
CHANGED
|
@@ -41,7 +41,7 @@ Agents should start from the packaged skill and capability commands instead of g
|
|
|
41
41
|
| `ima2 defaults --json` | Print the running server's effective model/reasoning defaults, falling back to local config when no server is reachable |
|
|
42
42
|
| `ima2 defaults --local --json` | Print local effective defaults without contacting the server |
|
|
43
43
|
|
|
44
|
-
`ima2 capabilities --json` separates supported and unsupported model ids. Agents should use only `valid.imageModels.supported` for generation/default choices. `limits.
|
|
44
|
+
`ima2 capabilities --json` separates supported and unsupported model ids. Agents should use only `valid.imageModels.supported` for generation/default choices. `limits.maxGeneratedImages` reports the configured per-request image count limit, and `limits.maxParallel` reports the enforced server-side inflight capacity guard.
|
|
45
45
|
|
|
46
46
|
## Generation
|
|
47
47
|
|
|
@@ -61,7 +61,7 @@ Provider override semantics:
|
|
|
61
61
|
- `api` forces the API-key Responses path and requires a configured API key.
|
|
62
62
|
- `oauth` forces the local OAuth proxy path.
|
|
63
63
|
- `grok` uses the bundled progrok xAI proxy (`127.0.0.1:18645`). Classic generation first runs mandatory xAI Web Search through Responses API, then asks `grok-4.3` to call ima2's local `generate_image` tool, then ima2 executes xAI `/v1/images/generations`. If `--ref` images are attached, the final step uses xAI `/v1/images/edits` instead so image-to-image/reference context is preserved. Models: `grok-imagine-image`, `grok-imagine-image-quality`. Size is mapped to xAI `aspect_ratio` and `resolution`; the UI web-search toggle is OpenAI-provider-only because Grok search is always on in this path.
|
|
64
|
-
- `agy` spawns the Antigravity CLI to generate via Google Gemini (`nano-banana-2`). Fixed 1024×1024 JPEG output, max 3 refs. No web search, quality, size, or mask controls.
|
|
64
|
+
- `agy` spawns the Antigravity CLI to generate via Google Gemini (`nano-banana-2`). Fixed 1024×1024 JPEG output, max 3 refs. No web search, quality, size, or mask controls. If `agy` is not on the server process PATH, ima2 also checks common user-local installs such as `~/.local/bin/agy`; set `IMA2_AGY_BIN=/absolute/path/to/agy` to force a specific binary.
|
|
65
65
|
- `gemini-api` calls the Google Generative Language API directly. Models: `nano-banana-2` (Gemini 3.1 Flash Image) and `nano-banana-pro` (Gemini 3 Pro Image). Use `--model nano-banana-2` or `--model nano-banana-pro` to select. Supports `--size` for aspect ratio and resolution (512px–4K) on the direct API path; Vertex AI ignores aspect/size. Requires `GEMINI_API_KEY` or a Vertex AI service account (`VERTEX_SERVICE_ACCOUNT_JSON`). Switching from `agy` or `gemini-api` provider auto-selects the corresponding Gemini model; switching away resets to the GPT default.
|
|
66
66
|
- `auto` preserves route default behavior and currently resolves to GPT OAuth unless server routing changes.
|
|
67
67
|
|
|
@@ -108,7 +108,7 @@ mockup`.
|
|
|
108
108
|
For dense or critical text, keep the text large and explicit. Exact placement,
|
|
109
109
|
small text, and pixel-perfect typography can still need iteration or post-editing.
|
|
110
110
|
|
|
111
|
-
Multimode-specific flags include `--max-images <1..
|
|
111
|
+
Multimode-specific flags include `--max-images <1..24>` by default (configurable through `IMA2_MAX_GENERATED_IMAGES`), `--ref <file>` (repeatable, max 5), `--mode <auto|direct>`, `--provider <auto|oauth|api|grok|grok-api|agy|gemini-api>`, and `--show-partial`. `ima2 edit --mask` remains intentionally deferred to #31 because current mask plumbing is guided edit rather than guaranteed true masked/inpaint semantics.
|
|
112
112
|
|
|
113
113
|
## Video
|
|
114
114
|
|
|
@@ -362,7 +362,7 @@ comfy.{defaultUrl,uploadTimeoutMs,maxUploadBytes}
|
|
|
362
362
|
storage.{generatedDir,generatedDirName}
|
|
363
363
|
server.{port,host,bodyLimit}
|
|
364
364
|
oauth.{proxyPort,statusTimeoutMs,restartDelayMs}
|
|
365
|
-
limits.{maxRefCount,maxParallel}
|
|
365
|
+
limits.{maxRefCount,maxGeneratedImages,maxParallel}
|
|
366
366
|
history.{defaultPageSize,maxPageCap}
|
|
367
367
|
```
|
|
368
368
|
|
package/docs/FAQ.ko.md
CHANGED
|
@@ -337,7 +337,7 @@ GPT OAuth는 OpenAI와 ChatGPT/Codex 관련 호스트 접근이 필요할 수
|
|
|
337
337
|
|
|
338
338
|
### 동시 작업 상한은 얼마인가요?
|
|
339
339
|
|
|
340
|
-
서버는 동시 생성 작업을
|
|
340
|
+
서버는 동시 생성 작업을 설정값 `limits.maxParallel` 기준으로 제한합니다(기본 `24`, `IMA2_MAX_PARALLEL`로 변경 가능). 초과 요청은 `429`와 `Retry-After: 5`를 받습니다. SSE 엔드포인트 자체는 512개 동시 연결까지 지원합니다.
|
|
341
341
|
|
|
342
342
|
## CLI 점검 순서
|
|
343
343
|
|
package/docs/FAQ.md
CHANGED
|
@@ -362,7 +362,7 @@ The event channel client reconnects automatically with exponential backoff. On r
|
|
|
362
362
|
|
|
363
363
|
### What is the maximum number of concurrent jobs?
|
|
364
364
|
|
|
365
|
-
The server caps concurrent generation jobs at
|
|
365
|
+
The server caps concurrent generation jobs at the configured `limits.maxParallel` value (default `24`, overridable with `IMA2_MAX_PARALLEL`). Additional requests receive `429` with `Retry-After: 5`. The SSE endpoint itself caps at 512 simultaneous connections.
|
|
366
366
|
|
|
367
367
|
## CLI troubleshooting checklist
|
|
368
368
|
|
|
@@ -4,16 +4,20 @@ Generated by `npm run test:inventory` (script: `scripts/classify-tests.mjs`).
|
|
|
4
4
|
|
|
5
5
|
_Tests considered "runtime-importing" if they import from `../lib/`, `../routes/`, `../bin/`, `../server`, or `../config`._
|
|
6
6
|
|
|
7
|
-
Total:
|
|
7
|
+
Total: 201 (runtime: 71, contract: 130)
|
|
8
8
|
|
|
9
9
|
## Runtime-importing tests
|
|
10
10
|
- `tests/agent-mode-auto-planner-contract.test.ts`
|
|
11
|
+
- `tests/agent-mode-llm-planner-contract.test.ts`
|
|
11
12
|
- `tests/agent-mode-parallel-contract.test.ts`
|
|
12
13
|
- `tests/agent-mode-queue-contract.test.ts`
|
|
13
14
|
- `tests/agent-mode-queue-migration-contract.test.ts`
|
|
14
15
|
- `tests/agent-mode-runtime-contract.test.ts`
|
|
15
16
|
- `tests/agent-mode-slash-command-contract.test.ts`
|
|
17
|
+
- `tests/agent-mode-ux-feedback-contract.test.ts`
|
|
16
18
|
- `tests/agent-video-intent.test.ts`
|
|
19
|
+
- `tests/agy-artifact-fallback.test.ts`
|
|
20
|
+
- `tests/agy-cli.test.ts`
|
|
17
21
|
- `tests/api-cache-policy.test.ts`
|
|
18
22
|
- `tests/api-provider-parity.test.ts`
|
|
19
23
|
- `tests/billing-source.test.ts`
|
|
@@ -29,6 +33,7 @@ Total: 191 (runtime: 66, contract: 125)
|
|
|
29
33
|
- `tests/generate-route-validation-error.test.ts`
|
|
30
34
|
- `tests/generated-static-privacy.test.ts`
|
|
31
35
|
- `tests/generation-errors.test.ts`
|
|
36
|
+
- `tests/generation-request-log.test.ts`
|
|
32
37
|
- `tests/grok-command-login-contract.test.ts`
|
|
33
38
|
- `tests/grok-planner-adapter.test.ts`
|
|
34
39
|
- `tests/grok-proxy-launcher.test.ts`
|
|
@@ -78,6 +83,7 @@ Total: 191 (runtime: 66, contract: 125)
|
|
|
78
83
|
- `tests/agent-mode-frontend-contract.test.js`
|
|
79
84
|
- `tests/agent-mode-layout-contract.test.js`
|
|
80
85
|
- `tests/agent-mode-right-sidebar-contract.test.js`
|
|
86
|
+
- `tests/agent-mode-run-progress-contract.test.ts`
|
|
81
87
|
- `tests/agent-mode-tool-folding-contract.test.js`
|
|
82
88
|
- `tests/app-weight-splitting-contract.test.js`
|
|
83
89
|
- `tests/async-capacity-retry-behavior.test.ts`
|
|
@@ -100,6 +106,7 @@ Total: 191 (runtime: 66, contract: 125)
|
|
|
100
106
|
- `tests/canvas-hit-test-contract.test.js`
|
|
101
107
|
- `tests/canvas-mask-contract.test.js`
|
|
102
108
|
- `tests/canvas-memo-contract.test.js`
|
|
109
|
+
- `tests/canvas-memo-prompt-contract.test.js`
|
|
103
110
|
- `tests/canvas-mode-contract.test.js`
|
|
104
111
|
- `tests/canvas-object-key-contract.test.js`
|
|
105
112
|
- `tests/canvas-persistence-contract.test.js`
|
|
@@ -142,6 +149,7 @@ Total: 191 (runtime: 66, contract: 125)
|
|
|
142
149
|
- `tests/gallery-shortcuts-visible-domain-contract.test.js`
|
|
143
150
|
- `tests/gallery-viewer-navigation-contract.test.js`
|
|
144
151
|
- `tests/generation-controls-ux-contract.test.js`
|
|
152
|
+
- `tests/generation-limit-unlock-contract.test.js`
|
|
145
153
|
- `tests/health.test.js`
|
|
146
154
|
- `tests/history-index-contract.test.js`
|
|
147
155
|
- `tests/history-permanent-delete.test.js`
|
|
@@ -165,6 +173,7 @@ Total: 191 (runtime: 66, contract: 125)
|
|
|
165
173
|
- `tests/node-diagnostics-contract.test.js`
|
|
166
174
|
- `tests/node-edge-disconnect-contract.test.js`
|
|
167
175
|
- `tests/node-footer-compact-contract.test.js`
|
|
176
|
+
- `tests/node-gallery-drop-contract.test.js`
|
|
168
177
|
- `tests/node-generation-lock-contract.test.js`
|
|
169
178
|
- `tests/node-layout-contract.test.js`
|
|
170
179
|
- `tests/node-pending-recovery-contract.test.js`
|
|
@@ -184,6 +193,7 @@ Total: 191 (runtime: 66, contract: 125)
|
|
|
184
193
|
- `tests/prompt-library-ui-contract.test.js`
|
|
185
194
|
- `tests/prompt-studio-docs-contract.test.js`
|
|
186
195
|
- `tests/prompt-studio-ui-contract.test.js`
|
|
196
|
+
- `tests/provider-ui-polish-contract.test.js`
|
|
187
197
|
- `tests/server-fallback-contract.test.js`
|
|
188
198
|
- `tests/server.test.js`
|
|
189
199
|
- `tests/session-conflict.test.js`
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { config } from "../config.js";
|
|
2
|
+
const MAX_AGENT_VARIANT_COUNT = Math.max(1, Math.trunc(Number(config.limits.maxGeneratedImages) || 24));
|
|
3
|
+
const MAX_AGENT_PARALLELISM = Math.max(1, Math.trunc(Number(config.limits.maxParallel) || 24));
|
|
1
4
|
const COMMAND_ALIASES = {
|
|
2
5
|
ask: "question",
|
|
3
6
|
q: "question",
|
|
@@ -28,7 +31,7 @@ export function parseAgentSlashCommand(input) {
|
|
|
28
31
|
return null;
|
|
29
32
|
const rest = (match[2] ?? "").trim();
|
|
30
33
|
const countCommands = ["variants", "generate", "parallelism"];
|
|
31
|
-
const parsed = countCommands.includes(name) ? parseLeadingCount(rest) : { prompt: rest };
|
|
34
|
+
const parsed = countCommands.includes(name) ? parseLeadingCount(rest, name) : { prompt: rest };
|
|
32
35
|
return {
|
|
33
36
|
name,
|
|
34
37
|
rawName,
|
|
@@ -41,9 +44,9 @@ export function formatAgentSlashHelp() {
|
|
|
41
44
|
return [
|
|
42
45
|
"Available Agent commands:",
|
|
43
46
|
"/question <topic> - ask a planning question without generating images.",
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
+
`/variants <1-${MAX_AGENT_VARIANT_COUNT}> <prompt> - force that many image variants.`,
|
|
48
|
+
`/generate <1-${MAX_AGENT_VARIANT_COUNT}> <prompt> - generate a bounded fanout from one request.`,
|
|
49
|
+
`/parallelism <1-${MAX_AGENT_PARALLELISM}> <prompt> - cap concurrent tool calls for this turn.`,
|
|
47
50
|
].join("\n");
|
|
48
51
|
}
|
|
49
52
|
export function formatAgentQuestionReply(prompt) {
|
|
@@ -53,12 +56,13 @@ export function formatAgentQuestionReply(prompt) {
|
|
|
53
56
|
}
|
|
54
57
|
return topic;
|
|
55
58
|
}
|
|
56
|
-
function parseLeadingCount(value) {
|
|
59
|
+
function parseLeadingCount(value, name) {
|
|
57
60
|
const match = /^(\d+)(?:\s+([\s\S]*))?$/.exec(value);
|
|
58
61
|
if (!match)
|
|
59
62
|
return { prompt: value };
|
|
63
|
+
const max = name === "parallelism" ? MAX_AGENT_PARALLELISM : MAX_AGENT_VARIANT_COUNT;
|
|
60
64
|
return {
|
|
61
|
-
value: Math.max(1, Math.min(
|
|
65
|
+
value: Math.max(1, Math.min(max, Number(match[1]))),
|
|
62
66
|
prompt: (match[2] ?? "").trim(),
|
|
63
67
|
};
|
|
64
68
|
}
|
|
@@ -1,25 +1,35 @@
|
|
|
1
|
-
|
|
1
|
+
import { config } from "../config.js";
|
|
2
|
+
const HARD_MAX_VARIANTS = Math.max(1, Math.trunc(Number(config.limits.maxGeneratedImages) || 24));
|
|
2
3
|
const AMBIGUOUS_MULTI_VARIANTS = 3;
|
|
3
4
|
const KOREAN_COUNT_WORDS = [
|
|
4
|
-
[/(
|
|
5
|
-
[/(
|
|
6
|
-
[/(
|
|
7
|
-
[/(
|
|
8
|
-
[/(
|
|
9
|
-
[/(
|
|
10
|
-
[/(
|
|
11
|
-
[/(
|
|
5
|
+
[/(?:스물네|스물\s*네|이십사)\s*(?:장|개|가지|컷|시안|버전)/u, 24],
|
|
6
|
+
[/(?:열두|열\s*두|십이)\s*(?:장|개|가지|컷|시안|버전)/u, 12],
|
|
7
|
+
[/(?:열|열\s*개)\s*(?:장|개|가지|컷|시안|버전)?/u, 10],
|
|
8
|
+
[/(?:한|하나)\s*(?:장|개|가지|컷|시안|버전)/u, 1],
|
|
9
|
+
[/(?:두|둘)\s*(?:장|개|가지|컷|시안|버전)/u, 2],
|
|
10
|
+
[/(?:세|셋)\s*(?:장|개|가지|컷|시안|버전)/u, 3],
|
|
11
|
+
[/(?:네|넷)\s*(?:장|개|가지|컷|시안|버전)/u, 4],
|
|
12
|
+
[/(?:다섯)\s*(?:장|개|가지|컷|시안|버전)/u, 5],
|
|
13
|
+
[/(?:여섯)\s*(?:장|개|가지|컷|시안|버전)/u, 6],
|
|
14
|
+
[/(?:일곱)\s*(?:장|개|가지|컷|시안|버전)/u, 7],
|
|
15
|
+
[/(?:여덟)\s*(?:장|개|가지|컷|시안|버전)/u, 8],
|
|
16
|
+
[/(?:아홉)\s*(?:장|개|가지|컷|시안|버전)/u, 9],
|
|
12
17
|
];
|
|
13
18
|
const ENGLISH_COUNT_WORDS = [
|
|
14
|
-
[/\
|
|
15
|
-
[/\
|
|
16
|
-
[/\
|
|
17
|
-
[/\
|
|
18
|
-
[/\
|
|
19
|
-
[/\
|
|
20
|
-
[/\
|
|
21
|
-
[/\
|
|
19
|
+
[/\btwenty[-\s]?four\s*(?:image|variant|version|option|candidate|shot|render)?s?\b/iu, 24],
|
|
20
|
+
[/\btwelve\s*(?:image|variant|version|option|candidate|shot|render)?s?\b/iu, 12],
|
|
21
|
+
[/\bten\s*(?:image|variant|version|option|candidate|shot|render)?s?\b/iu, 10],
|
|
22
|
+
[/\bone\s*(?:image|variant|version|option|candidate|shot|render)?s?\b/iu, 1],
|
|
23
|
+
[/\btwo\s*(?:image|variant|version|option|candidate|shot|render)?s?\b/iu, 2],
|
|
24
|
+
[/\bthree\s*(?:image|variant|version|option|candidate|shot|render)?s?\b/iu, 3],
|
|
25
|
+
[/\bfour\s*(?:image|variant|version|option|candidate|shot|render)?s?\b/iu, 4],
|
|
26
|
+
[/\bfive\s*(?:image|variant|version|option|candidate|shot|render)?s?\b/iu, 5],
|
|
27
|
+
[/\bsix\s*(?:image|variant|version|option|candidate|shot|render)?s?\b/iu, 6],
|
|
28
|
+
[/\bseven\s*(?:image|variant|version|option|candidate|shot|render)?s?\b/iu, 7],
|
|
29
|
+
[/\beight\s*(?:image|variant|version|option|candidate|shot|render)?s?\b/iu, 8],
|
|
30
|
+
[/\bnine\s*(?:image|variant|version|option|candidate|shot|render)?s?\b/iu, 9],
|
|
22
31
|
];
|
|
32
|
+
const NUMERIC_COUNT_PATTERN = /(?:^|\b)(\d{1,3})\s*(?:장|개|가지|컷|시안|버전|image|images|variant|variants|version|versions|option|options|candidate|candidates|shot|shots|render|renders)(?:\b|$)/iu;
|
|
23
33
|
export function deriveAgentGenerationPlan({ prompt, settings, command = null }) {
|
|
24
34
|
if (command?.name === "question" || command?.name === "help") {
|
|
25
35
|
return {
|
|
@@ -32,6 +42,7 @@ export function deriveAgentGenerationPlan({ prompt, settings, command = null })
|
|
|
32
42
|
reason: command.name === "help" ? "Slash help answered without image generation." : "Question command answered without image generation.",
|
|
33
43
|
command: command.name,
|
|
34
44
|
assistantText: null,
|
|
45
|
+
sourceImagePolicy: null,
|
|
35
46
|
};
|
|
36
47
|
}
|
|
37
48
|
if (isVideoIntent(prompt)) {
|
|
@@ -45,6 +56,7 @@ export function deriveAgentGenerationPlan({ prompt, settings, command = null })
|
|
|
45
56
|
reason: "Video generation detected from prompt keywords.",
|
|
46
57
|
command: command?.name ?? null,
|
|
47
58
|
assistantText: null,
|
|
59
|
+
sourceImagePolicy: "auto",
|
|
48
60
|
};
|
|
49
61
|
}
|
|
50
62
|
const variantDecision = decideVariantCount(prompt, settings, command);
|
|
@@ -60,18 +72,41 @@ export function deriveAgentGenerationPlan({ prompt, settings, command = null })
|
|
|
60
72
|
reason: variantDecision.reason,
|
|
61
73
|
command: command?.name ?? null,
|
|
62
74
|
assistantText: null,
|
|
75
|
+
sourceImagePolicy: inferSourceImagePolicy(prompt, prompts.length > 1 ? "fanout" : "single"),
|
|
63
76
|
};
|
|
64
77
|
}
|
|
65
78
|
export function normalizeAgentGenerationPlan(prompt, value, settings) {
|
|
66
79
|
const input = value && typeof value === "object" ? value : {};
|
|
67
80
|
const prompts = cleanPromptArray(input.prompts);
|
|
81
|
+
// errors/question modes legitimately carry no generation prompts — they
|
|
82
|
+
// must be resolved before the empty-prompts fallback re-derives a regex plan.
|
|
83
|
+
if (input.mode === "errors" || input.mode === "question") {
|
|
84
|
+
return {
|
|
85
|
+
mode: input.mode,
|
|
86
|
+
prompts: [],
|
|
87
|
+
requestedVariants: 0,
|
|
88
|
+
plannedVariants: 0,
|
|
89
|
+
plannedParallelism: 0,
|
|
90
|
+
source: cleanPlanSource(input.source),
|
|
91
|
+
reason: cleanReason(input.reason, input.mode === "errors" ? "User asked about recent generation failures." : "User asked a question answered without generation."),
|
|
92
|
+
command: cleanCommandName(input.command),
|
|
93
|
+
assistantText: typeof input.assistantText === "string" ? input.assistantText : null,
|
|
94
|
+
videoParams: null,
|
|
95
|
+
sourceImagePolicy: null,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
68
98
|
if (prompts.length === 0)
|
|
69
99
|
return deriveAgentGenerationPlan({ prompt, settings, command: cleanCommand(input.command) });
|
|
70
100
|
const plannedVariants = cleanCount(input.plannedVariants, prompts.length, 0, HARD_MAX_VARIANTS);
|
|
71
101
|
const requestedParallelism = cleanCount(input.plannedParallelism, settings.parallelism, 1, HARD_MAX_VARIANTS);
|
|
72
102
|
const plannedParallelism = resolvePlannedParallelism({ ...settings, parallelism: requestedParallelism }, plannedVariants, null);
|
|
103
|
+
const mode = input.mode === "question"
|
|
104
|
+
? "question"
|
|
105
|
+
: input.mode === "video"
|
|
106
|
+
? "video"
|
|
107
|
+
: prompts.length > 1 ? "fanout" : "single";
|
|
73
108
|
return {
|
|
74
|
-
mode
|
|
109
|
+
mode,
|
|
75
110
|
prompts,
|
|
76
111
|
requestedVariants: cleanCount(input.requestedVariants, plannedVariants, 0, HARD_MAX_VARIANTS),
|
|
77
112
|
plannedVariants,
|
|
@@ -80,8 +115,24 @@ export function normalizeAgentGenerationPlan(prompt, value, settings) {
|
|
|
80
115
|
reason: cleanReason(input.reason, prompts.length > 1 ? "Stored fanout plan." : "Stored single-image plan."),
|
|
81
116
|
command: cleanCommandName(input.command),
|
|
82
117
|
assistantText: typeof input.assistantText === "string" ? input.assistantText : null,
|
|
118
|
+
videoParams: mode === "video" ? cleanVideoParams(input.videoParams) : null,
|
|
119
|
+
sourceImagePolicy: cleanSourceImagePolicy(input.sourceImagePolicy) ?? inferSourceImagePolicy(prompt, mode),
|
|
83
120
|
};
|
|
84
121
|
}
|
|
122
|
+
function inferSourceImagePolicy(prompt, mode) {
|
|
123
|
+
if (mode === "question" || mode === "errors")
|
|
124
|
+
return null;
|
|
125
|
+
if (mode === "video")
|
|
126
|
+
return "auto";
|
|
127
|
+
const text = prompt.trim();
|
|
128
|
+
if (/(?:i2i\s*말고|image-to-image\s*말고|새로운\s*방식|새로|별도|독립|from\s+scratch|new\s+image|fresh\s+image|without\s+(?:reference|refs?)|no\s+(?:reference|refs?))/iu.test(text)) {
|
|
129
|
+
return "none";
|
|
130
|
+
}
|
|
131
|
+
if (/(?:이\s*이미지|현재\s*이미지|방금\s*(?:그거|그\s*이미지|이미지)|참조|레퍼런스|reference|refs?|i2i|image-to-image|수정|편집|바꿔|변형|유지(?:해서)?|그걸|그거)/iu.test(text)) {
|
|
132
|
+
return "current";
|
|
133
|
+
}
|
|
134
|
+
return "none";
|
|
135
|
+
}
|
|
85
136
|
function decideVariantCount(prompt, settings, command) {
|
|
86
137
|
if (command?.name === "variants" || command?.name === "generate") {
|
|
87
138
|
const count = clampCount(command.value ?? AMBIGUOUS_MULTI_VARIANTS, HARD_MAX_VARIANTS);
|
|
@@ -114,6 +165,11 @@ function decideVariantCount(prompt, settings, command) {
|
|
|
114
165
|
}
|
|
115
166
|
function inferRequestedVariantCount(prompt) {
|
|
116
167
|
const text = prompt.trim();
|
|
168
|
+
const numericMatch = NUMERIC_COUNT_PATTERN.exec(text);
|
|
169
|
+
if (numericMatch) {
|
|
170
|
+
const count = Math.max(1, Math.trunc(Number.parseInt(numericMatch[1] ?? "", 10) || 1));
|
|
171
|
+
return { count, reason: `User request explicitly implies ${count} variant${count === 1 ? "" : "s"}.` };
|
|
172
|
+
}
|
|
117
173
|
for (const [pattern, count] of [...KOREAN_COUNT_WORDS, ...ENGLISH_COUNT_WORDS]) {
|
|
118
174
|
if (pattern.test(text)) {
|
|
119
175
|
return { count, reason: `User request explicitly implies ${count} variant${count === 1 ? "" : "s"}.` };
|
|
@@ -156,10 +212,31 @@ function cleanPlanSource(value) {
|
|
|
156
212
|
value === "auto-request" ||
|
|
157
213
|
value === "manual-settings" ||
|
|
158
214
|
value === "slash-command" ||
|
|
159
|
-
value === "question-command"
|
|
215
|
+
value === "question-command" ||
|
|
216
|
+
value === "llm-planner")
|
|
160
217
|
return value;
|
|
161
218
|
return "auto-default";
|
|
162
219
|
}
|
|
220
|
+
function cleanSourceImagePolicy(value) {
|
|
221
|
+
if (value === "auto" || value === "none" || value === "current")
|
|
222
|
+
return value;
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
export function cleanVideoParams(value) {
|
|
226
|
+
if (!value || typeof value !== "object")
|
|
227
|
+
return null;
|
|
228
|
+
const input = value;
|
|
229
|
+
const params = {};
|
|
230
|
+
const duration = typeof input.duration === "number" ? input.duration : Number(input.duration);
|
|
231
|
+
if (Number.isFinite(duration))
|
|
232
|
+
params.duration = Math.max(1, Math.min(15, Math.round(duration)));
|
|
233
|
+
if (input.resolution === "480p" || input.resolution === "720p")
|
|
234
|
+
params.resolution = input.resolution;
|
|
235
|
+
if (typeof input.aspectRatio === "string" && /^(auto|16:9|9:16|4:3|3:4|3:2|2:3|1:1)$/.test(input.aspectRatio)) {
|
|
236
|
+
params.aspectRatio = input.aspectRatio;
|
|
237
|
+
}
|
|
238
|
+
return Object.keys(params).length > 0 ? params : null;
|
|
239
|
+
}
|
|
163
240
|
function cleanCommandName(value) {
|
|
164
241
|
if (value === "question" ||
|
|
165
242
|
value === "help" ||
|