@lazyingart/agintiflow 0.20.184 → 0.20.185

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.
@@ -7,6 +7,10 @@ AgInTiFlow separates **skills** from **tools**:
7
7
 
8
8
  The optional image-generation skills are `image_generation` and `venice_image_generation`. Both use the deterministic `generate_image` tool.
9
9
 
10
+ `generate_image` is a raster-image tool. It does not produce true SVG/vector output. If a caller requests `svg`, the tool records
11
+ `requestedFormat: "svg"`, generates a PNG fallback with `actualFormat: "png"`, and returns a `formatNotice`. If the task truly requires
12
+ editable vectors, use AgInTiFlow file tools to write deterministic SVG/LaTeX/HTML instead of calling `generate_image`.
13
+
10
14
  ## Setup
11
15
 
12
16
  Store a GRS AI or Venice key project-locally:
@@ -38,11 +42,39 @@ For image, cover, poster, illustration, photo, or logo-concept requests, the mod
38
42
  "model": "nano-banana-2",
39
43
  "outputDir": "artifacts/images/robot-cover",
40
44
  "outputStem": "robot-cover",
45
+ "format": "png",
41
46
  "aspectRatio": "1:1",
42
47
  "imageSize": "2K"
43
48
  }
44
49
  ```
45
50
 
51
+ For trusted local apps that need image generation without routing through a language-model run, the web server exposes a deterministic endpoint:
52
+
53
+ ```bash
54
+ curl -sS http://127.0.0.1:3210/api/auxiliary/generate-image \
55
+ -H 'Content-Type: application/json' \
56
+ -d '{
57
+ "prompt": "A polished cyan robot painting a circuit-board river",
58
+ "provider": "venice",
59
+ "format": "svg",
60
+ "outputDir": "artifacts/images/robot-cover",
61
+ "outputStem": "robot-cover"
62
+ }'
63
+ ```
64
+
65
+ This endpoint is intended for trusted local/server-side callers. Browser apps on another origin should use an explicit adapter/proxy
66
+ rather than relying on permissive CORS, because generation can consume API keys and write workspace artifacts.
67
+
68
+ That SVG request returns a PNG fallback contract rather than pretending SVG was generated:
69
+
70
+ ```json
71
+ {
72
+ "requestedFormat": "svg",
73
+ "actualFormat": "png",
74
+ "formatNotice": "SVG/vector output is not supported by generate_image. A raster PNG is generated instead."
75
+ }
76
+ ```
77
+
46
78
  With GRS AI, the tool uses the Nano Banana API:
47
79
 
48
80
  - `POST https://grsaiapi.com/v1/draw/nano-banana`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lazyingart/agintiflow",
3
- "version": "0.20.184",
3
+ "version": "0.20.185",
4
4
  "type": "module",
5
5
  "description": "AgInTiFlow is a project-aware agent workspace for hybrid wet-dry R&D, hardware-aware intelligence, software automation, and industrial workflows.",
6
6
  "license": "Apache-2.0",
@@ -2,12 +2,24 @@
2
2
 
3
3
  Date: 2026-05-20
4
4
 
5
+ Updated: 2026-05-31
6
+
5
7
  This note records the working publication route for AgInTiFlow so future release work does not get blocked by stale local npm tokens or repeated discussion about why direct `npm publish` failed.
6
8
 
7
9
  ## Published Evidence
8
10
 
9
11
  Latest verified release:
10
12
 
13
+ - Version: `0.20.184`
14
+ - Commit: `9b22f4c`
15
+ - GitHub Actions run: `https://github.com/lazyingart/AgInTiFlow/actions/runs/26700154894`
16
+ - Workflow result: success
17
+ - npm registry check: `npm view @lazyingart/agintiflow version dist-tags.latest --registry=https://registry.npmjs.org` returned `0.20.184`.
18
+ - Installed verification: `npm install -g @lazyingart/agintiflow@0.20.184` then `aginti --version` returned `0.20.184`.
19
+ - Webapp verification: `aginti webapp restart --port 3210` then `curl -fsS http://127.0.0.1:3210/health` returned `version":"0.20.184"` from the global npm package path.
20
+
21
+ Previous verified release:
22
+
11
23
  - Version: `0.20.182`
12
24
  - Commit: `b5492f7`
13
25
  - GitHub Actions run: `https://github.com/lazyingart/AgInTiFlow/actions/runs/26198123298`
@@ -60,6 +60,27 @@ try {
60
60
  }
61
61
  );
62
62
  assert(veniceDryRun.ok && veniceDryRun.provider === "venice", "venice generate_image dry run failed");
63
+ const svgFallbackDryRun = await generateImage(
64
+ {
65
+ provider: "venice",
66
+ prompt: "A simple geometric logo requested as SVG.",
67
+ outputDir: "artifacts/images/svg-fallback-dry-run",
68
+ outputStem: "logo.svg",
69
+ format: "svg",
70
+ dryRun: true,
71
+ },
72
+ {
73
+ commandCwd: workspace,
74
+ allowFileTools: true,
75
+ }
76
+ );
77
+ assert(svgFallbackDryRun.ok && svgFallbackDryRun.requestedFormat === "svg", "SVG fallback did not record requestedFormat");
78
+ assert(svgFallbackDryRun.actualFormat === "png", "SVG fallback did not select PNG output");
79
+ assert(/raster PNG/i.test(svgFallbackDryRun.formatNotice || ""), "SVG fallback did not explain the PNG fallback");
80
+ const svgFallbackManifest = JSON.parse(
81
+ await fs.readFile(path.join(workspace, "artifacts/images/svg-fallback-dry-run/task_manifest.json"), "utf8")
82
+ );
83
+ assert(svgFallbackManifest.requestedFormat === "svg" && svgFallbackManifest.actualFormat === "png", "SVG fallback manifest missing format contract");
63
84
 
64
85
  const blocked = await generateImage(
65
86
  {
@@ -109,6 +130,7 @@ try {
109
130
  "venice_image_skill_listed",
110
131
  "generate_image_dry_run",
111
132
  "venice_generate_image_dry_run",
133
+ "svg_request_png_fallback",
112
134
  "generate_image_guardrail",
113
135
  "mock_agent_image_tool",
114
136
  ],
@@ -178,6 +178,23 @@ try {
178
178
  }
179
179
  const traversalResponse = await fetch(`${baseUrl}/api/workspace/raw?commandCwd=${encodeURIComponent(runtimeDir)}&path=../escape.txt`);
180
180
  if (traversalResponse.ok) throw new Error("workspace raw endpoint allowed path traversal");
181
+ const imageApiDryRun = await fetchJson("/api/auxiliary/generate-image", {
182
+ method: "POST",
183
+ headers: { "Content-Type": "application/json" },
184
+ body: JSON.stringify({
185
+ commandCwd: runtimeDir,
186
+ provider: "venice",
187
+ prompt: "A compact logo requested as SVG for API fallback testing.",
188
+ format: "svg",
189
+ outputDir: "artifacts/images/api-svg-fallback",
190
+ outputStem: "api-logo.svg",
191
+ dryRun: true,
192
+ }),
193
+ });
194
+ if (!imageApiDryRun.ok || imageApiDryRun.requestedFormat !== "svg" || imageApiDryRun.actualFormat !== "png") {
195
+ throw new Error(`direct auxiliary image API did not return SVG-to-PNG fallback: ${JSON.stringify(imageApiDryRun)}`);
196
+ }
197
+ await fs.access(path.join(runtimeDir, "artifacts/images/api-svg-fallback/task_manifest.json"));
181
198
  if (config.preferences?.sandboxMode !== "docker-workspace") throw new Error("web did not default to docker workspace");
182
199
  if (config.preferences?.packageInstallPolicy !== "allow") throw new Error("web did not default to Docker package installs");
183
200
  if (config.preferences?.permissionMode !== "normal") throw new Error("web did not default to normal permission mode");
@@ -572,6 +589,7 @@ try {
572
589
  "POST /api/workspace/write",
573
590
  "POST /api/workspace/rename",
574
591
  "POST /api/workspace/delete",
592
+ "POST /api/auxiliary/generate-image",
575
593
  "/api/workspace/changes",
576
594
  "stale running status reconciliation",
577
595
  "/api/sessions/:id/artifacts",
@@ -629,7 +629,7 @@ async function createInitialState(config, sessionId) {
629
629
  config.allowAuxiliaryTools
630
630
  ? `Auxiliary skills are available: ${listAuxiliarySkills()
631
631
  .map((skill) => `${skill.id} via ${skill.toolName} (${skill.available ? "key available" : `needs ${skill.keyName}`})`)
632
- .join(", ")}. Use generate_image for real raster image/photo/illustration/cover/poster/logo requests when appropriate; if image keys are missing, ask the user to run /auxiliary grsai, aginti login grsai, or aginti login venice.`
632
+ .join(", ")}. Use generate_image for real raster image/photo/illustration/cover/poster/logo requests when appropriate. generate_image is raster-only; if SVG/vector is requested, either create true SVG/LaTeX/HTML with file tools or call generate_image with format=png and explicitly report requestedFormat=svg, actualFormat=png. If image keys are missing, ask the user to run /auxiliary grsai, aginti login grsai, or aginti login venice.`
633
633
  : "Auxiliary skills are disabled for this run.",
634
634
  config.allowWebSearch
635
635
  ? "web_search is available for lightweight snippets. web_research is available for sourced, persisted research artifacts; use domains for official-source constraints and mode=openai only when hosted OpenAI web research is needed and configured. Prefer these tools over browser search-engine navigation."
@@ -10,6 +10,7 @@ const DEFAULT_IMAGE_MODEL = "nano-banana-2";
10
10
  const DEFAULT_VENICE_IMAGE_MODEL = "nano-banana-2";
11
11
  const DEFAULT_ASPECT_RATIO = "1:1";
12
12
  const DEFAULT_IMAGE_SIZE = "2K";
13
+ const DEFAULT_OUTPUT_FORMAT = "png";
13
14
  const TRANSIENT_HTTP_CODES = new Set([408, 425, 429, 500, 502, 503, 504]);
14
15
  const MAX_PROMPT_BYTES = 120_000;
15
16
  const MAX_REFERENCE_BYTES = 4_000_000;
@@ -42,11 +43,45 @@ function timestampSlug() {
42
43
  function safeStem(value = "image") {
43
44
  const stem = String(value || "image")
44
45
  .trim()
46
+ .replace(/\.(?:svg|png|jpe?g|webp|gif)$/i, "")
45
47
  .replace(/[^A-Za-z0-9._-]+/g, "-")
46
48
  .replace(/^-+|-+$/g, "");
47
49
  return stem || "image";
48
50
  }
49
51
 
52
+ function normalizeOutputFormat(args = {}) {
53
+ const requested = String(args.outputFormat || args.format || DEFAULT_OUTPUT_FORMAT)
54
+ .trim()
55
+ .toLowerCase()
56
+ .replace(/^image\//, "");
57
+ if (requested === "svg" || requested === "svg+xml" || requested === "vector") {
58
+ return {
59
+ requestedFormat: requested === "svg+xml" ? "svg" : requested,
60
+ actualFormat: "png",
61
+ notice: "SVG/vector output is not supported by generate_image. A raster PNG is generated instead.",
62
+ };
63
+ }
64
+ if (requested === "webp") {
65
+ return {
66
+ requestedFormat: "webp",
67
+ actualFormat: "webp",
68
+ notice: "",
69
+ };
70
+ }
71
+ if (["jpg", "jpeg", "gif"].includes(requested)) {
72
+ return {
73
+ requestedFormat: requested,
74
+ actualFormat: "png",
75
+ notice: `${requested.toUpperCase()} output is not currently exposed by generate_image. A raster PNG is generated instead.`,
76
+ };
77
+ }
78
+ return {
79
+ requestedFormat: requested || DEFAULT_OUTPUT_FORMAT,
80
+ actualFormat: DEFAULT_OUTPUT_FORMAT,
81
+ notice: "",
82
+ };
83
+ }
84
+
50
85
  function normalizeAspectRatio(value) {
51
86
  const text = String(value || DEFAULT_ASPECT_RATIO).trim();
52
87
  return /^(?:\d+:\d+|1:1|2:3|3:2|3:4|4:3|9:16|16:9)$/.test(text) ? text : DEFAULT_ASPECT_RATIO;
@@ -305,7 +340,7 @@ function veniceBaseUrl(value = "") {
305
340
  .replace(/\/+$/, "");
306
341
  }
307
342
 
308
- async function generateVeniceImages({ prompt, args, target, outputStem, manifest, manifestPath, signal = null }) {
343
+ async function generateVeniceImages({ prompt, args, target, outputStem, manifest, manifestPath, formatInfo, signal = null }) {
309
344
  const apiKey = veniceKey();
310
345
  if (!apiKey) {
311
346
  throw new Error("Missing VENICE_API_KEY. Run `aginti login venice` or `aginti keys set venice --stdin`.");
@@ -314,7 +349,7 @@ async function generateVeniceImages({ prompt, args, target, outputStem, manifest
314
349
  const model =
315
350
  String(args.model || process.env.AGINTI_AUX_MODEL || process.env.VENICE_IMAGE_MODEL || DEFAULT_VENICE_IMAGE_MODEL).trim() ||
316
351
  DEFAULT_VENICE_IMAGE_MODEL;
317
- const format = String(args.format || "png").trim().toLowerCase() === "webp" ? "webp" : "png";
352
+ const format = formatInfo.actualFormat === "webp" ? "webp" : "png";
318
353
  const sizing = veniceSizingForModel(model, args);
319
354
  const payload = {
320
355
  model,
@@ -356,6 +391,9 @@ async function generateVeniceImages({ prompt, args, target, outputStem, manifest
356
391
  manifest.provider = "venice";
357
392
  manifest.host = base;
358
393
  manifest.model = model;
394
+ manifest.requestedFormat = formatInfo.requestedFormat;
395
+ manifest.actualFormat = format;
396
+ if (formatInfo.notice) manifest.formatNotice = formatInfo.notice;
359
397
  manifest.status = "succeeded";
360
398
  manifest.finishedAt = new Date().toISOString();
361
399
  manifest.downloadedFiles = downloads;
@@ -372,7 +410,10 @@ async function generateVeniceImages({ prompt, args, target, outputStem, manifest
372
410
  promptPath: path.posix.join(target.relativePath, "prompt.txt"),
373
411
  requestPayloadPath: path.posix.join(target.relativePath, "request_payload.redacted.json"),
374
412
  taskId: resultPayload.id ? String(resultPayload.id) : "",
375
- summary: `${imagePaths.length} image(s) generated through Venice`,
413
+ requestedFormat: formatInfo.requestedFormat,
414
+ actualFormat: format,
415
+ formatNotice: formatInfo.notice,
416
+ summary: `${imagePaths.length} image(s) generated through Venice${formatInfo.notice ? ` (${formatInfo.notice})` : ""}`,
376
417
  };
377
418
  }
378
419
 
@@ -403,6 +444,7 @@ export async function generateImage(args = {}, config = {}) {
403
444
  }
404
445
 
405
446
  const provider = args.provider ? normalizeImageProvider(args.provider) : defaultImageProvider(config);
447
+ const formatInfo = normalizeOutputFormat(args);
406
448
  const host = provider === "venice" ? veniceBaseUrl(args.host) : String(args.host || DEFAULT_GRS_HOST).trim().replace(/\/+$/, "") || DEFAULT_GRS_HOST;
407
449
  const model =
408
450
  provider === "venice"
@@ -419,6 +461,7 @@ export async function generateImage(args = {}, config = {}) {
419
461
  webHook: "-1",
420
462
  shutProgress: Boolean(args.shutProgress),
421
463
  };
464
+ if (provider === "venice") payload.format = formatInfo.actualFormat;
422
465
 
423
466
  const redactedPayload = redactPayload(payload);
424
467
  redactedPayload.urls = references.map((item) => item.redacted);
@@ -433,6 +476,9 @@ export async function generateImage(args = {}, config = {}) {
433
476
  provider,
434
477
  host,
435
478
  model,
479
+ requestedFormat: formatInfo.requestedFormat,
480
+ actualFormat: formatInfo.actualFormat,
481
+ formatNotice: formatInfo.notice,
436
482
  outputDir: target.relativePath,
437
483
  promptFile: path.posix.join(target.relativePath, "prompt.txt"),
438
484
  requestPayloadRedacted: path.posix.join(target.relativePath, "request_payload.redacted.json"),
@@ -452,12 +498,15 @@ export async function generateImage(args = {}, config = {}) {
452
498
  promptPath: path.posix.join(target.relativePath, "prompt.txt"),
453
499
  requestPayloadPath: path.posix.join(target.relativePath, "request_payload.redacted.json"),
454
500
  imagePaths: [],
455
- summary: "Prepared redacted image-generation payload without calling the provider.",
501
+ requestedFormat: formatInfo.requestedFormat,
502
+ actualFormat: formatInfo.actualFormat,
503
+ formatNotice: formatInfo.notice,
504
+ summary: `Prepared redacted image-generation payload without calling the provider.${formatInfo.notice ? ` ${formatInfo.notice}` : ""}`,
456
505
  };
457
506
  }
458
507
 
459
508
  if (provider === "venice") {
460
- return generateVeniceImages({ prompt, args, target, outputStem, manifest, manifestPath, signal: config.abortSignal });
509
+ return generateVeniceImages({ prompt, args, target, outputStem, manifest, manifestPath, formatInfo, signal: config.abortSignal });
461
510
  }
462
511
 
463
512
  const apiKey = grsaiKey();
@@ -515,6 +564,9 @@ export async function generateImage(args = {}, config = {}) {
515
564
  }
516
565
 
517
566
  manifest.status = "succeeded";
567
+ manifest.requestedFormat = formatInfo.requestedFormat;
568
+ manifest.actualFormat = path.extname(imagePaths[0] || "").replace(/^\./, "") || formatInfo.actualFormat;
569
+ if (formatInfo.notice) manifest.formatNotice = formatInfo.notice;
518
570
  manifest.finishedAt = new Date().toISOString();
519
571
  manifest.downloadedFiles = downloads;
520
572
  await writeJson(manifestPath, manifest);
@@ -528,6 +580,9 @@ export async function generateImage(args = {}, config = {}) {
528
580
  promptPath: path.posix.join(target.relativePath, "prompt.txt"),
529
581
  requestPayloadPath: path.posix.join(target.relativePath, "request_payload.redacted.json"),
530
582
  taskId: String(taskId),
531
- summary: `${imagePaths.length} image(s) generated`,
583
+ requestedFormat: formatInfo.requestedFormat,
584
+ actualFormat: manifest.actualFormat,
585
+ formatNotice: formatInfo.notice,
586
+ summary: `${imagePaths.length} image(s) generated${formatInfo.notice ? ` (${formatInfo.notice})` : ""}`,
532
587
  };
533
588
  }
@@ -723,7 +723,7 @@ export async function createPlan(client, config, state) {
723
723
  config.allowAuxiliaryTools
724
724
  ? `Auxiliary skills are enabled: ${listAuxiliarySkills()
725
725
  .map((skill) => `${skill.id} via ${skill.toolName} (${skill.available ? "key available" : `needs ${skill.keyName}`})`)
726
- .join(", ")}. For raster image generation requests, plan to use generate_image when a GRSAI or Venice image key is available; otherwise ask the user to run /auxiliary grsai, aginti login grsai, or aginti login venice.`
726
+ .join(", ")}. For raster image generation requests, plan to use generate_image when a GRSAI or Venice image key is available. generate_image is raster-only; if SVG/vector is requested, either create true SVG/LaTeX/HTML with file tools or use PNG fallback and report requestedFormat=svg, actualFormat=png. Otherwise ask the user to run /auxiliary grsai, aginti login grsai, or aginti login venice.`
727
727
  : "Auxiliary skills are disabled for this run.",
728
728
  config.allowWebSearch
729
729
  ? "web_search is available for lightweight snippets. web_research is available for auditable sourced research with persisted artifacts; use mode=snippets by default and mode=openai only when hosted OpenAI web research is needed and configured. Prefer these tools over opening a search engine in the browser."
@@ -1578,7 +1578,7 @@ export async function requestNextStep(client, config, messages) {
1578
1578
  function: {
1579
1579
  name: "generate_image",
1580
1580
  description:
1581
- "Generate a raster image artifact through optional GRS AI Nano Banana or Venice image-generation skills. Use for user requests that explicitly need a real image/photo/illustration/cover/poster/logo concept rather than SVG/code-native graphics. Saves prompt, redacted payload, manifest, and downloaded images in the workspace. If keys are missing, ask the user to run /auxiliary grsai, aginti login grsai, or aginti login venice.",
1581
+ "Generate a raster image artifact through optional GRS AI Nano Banana or Venice image-generation skills. Use for user requests that explicitly need a real image/photo/illustration/cover/poster/logo concept rather than SVG/code-native graphics. If a caller requests SVG/vector through this tool, request PNG and report the SVG-to-PNG fallback; if true vector is required, write SVG/LaTeX/HTML deterministically with file tools instead. Saves prompt, redacted payload, manifest, and downloaded images in the workspace. If keys are missing, ask the user to run /auxiliary grsai, aginti login grsai, or aginti login venice.",
1582
1582
  parameters: {
1583
1583
  type: "object",
1584
1584
  properties: {
@@ -1590,6 +1590,11 @@ export async function requestNextStep(client, config, messages) {
1590
1590
  prompt: { type: "string", description: "Detailed image-generation prompt." },
1591
1591
  outputDir: { type: "string", description: "Workspace-relative output directory. Defaults to artifacts/images/<timestamp>." },
1592
1592
  outputStem: { type: "string", description: "Filename stem for downloaded images." },
1593
+ format: {
1594
+ type: "string",
1595
+ enum: ["png", "webp", "svg", "jpeg", "jpg", "gif"],
1596
+ description: "Requested output format. SVG/vector is not supported by generate_image and is returned as a PNG fallback with requestedFormat/actualFormat metadata.",
1597
+ },
1593
1598
  aspectRatio: { type: "string", description: "Aspect ratio such as 1:1, 16:9, 2:3, or 3:2." },
1594
1599
  imageSize: { type: "string", description: "Image size such as 1K, 2K, or 4K." },
1595
1600
  model: { type: "string", description: "Image model. Defaults to nano-banana-2; Venice also supports models such as gpt-image-2, wan-2-7-text-to-image, qwen-image-2, and bria-bg-remover." },
@@ -248,7 +248,7 @@ export const TASK_PROFILES = {
248
248
  id: "image",
249
249
  label: "Image generation",
250
250
  prompt:
251
- "For raster image, cover, poster, illustration, photo, and logo-concept tasks, write a clear visual prompt, use generate_image when the optional GRS AI or Venice image key is available, save outputs under artifacts/images, and send the selected image to the canvas. If keys are missing, ask the user to run /auxiliary grsai, aginti login grsai, or aginti login venice.",
251
+ "For raster image, cover, poster, illustration, photo, and logo-concept tasks, write a clear visual prompt, use generate_image when the optional GRS AI or Venice image key is available, save outputs under artifacts/images, and send the selected image to the canvas. generate_image is raster-only: if SVG/vector is requested, either create deterministic SVG/LaTeX/HTML with file tools or return a PNG fallback with requestedFormat=svg and actualFormat=png. If keys are missing, ask the user to run /auxiliary grsai, aginti login grsai, or aginti login venice.",
252
252
  tools: ["auxiliary:image_generation", "files", "canvas"],
253
253
  },
254
254
  aaps: {
package/web.js CHANGED
@@ -39,6 +39,7 @@ import {
39
39
  setProviderKey,
40
40
  } from "./src/project.js";
41
41
  import { buildCapabilityReport } from "./src/capabilities.js";
42
+ import { generateImage } from "./src/auxiliary-tools.js";
42
43
  import { listExternalSkillPacks, listSkills } from "./src/skill-library.js";
43
44
  import { platformInfo, platformLabel, platformSetupHints } from "./src/platform.js";
44
45
  import { normalizeLanguage } from "./src/i18n.js";
@@ -1585,6 +1586,28 @@ app.post("/api/mcp", async (req, res) => {
1585
1586
  }
1586
1587
  });
1587
1588
 
1589
+ app.post("/api/auxiliary/generate-image", async (req, res) => {
1590
+ try {
1591
+ const body = req.body || {};
1592
+ const preferences = normalizePreferencePayload({}, db.getPreferences());
1593
+ const config = buildRunConfig({
1594
+ ...preferences,
1595
+ ...body,
1596
+ goal: "auxiliary image generation",
1597
+ commandCwd: body.commandCwd || body.cwd || preferences.commandCwd || baseDir,
1598
+ allowFileTools: true,
1599
+ allowAuxiliaryTools: true,
1600
+ auxiliaryProvider: body.provider || body.auxiliaryProvider || preferences.auxiliaryProvider,
1601
+ auxiliaryModel: body.model || body.auxiliaryModel || preferences.auxiliaryModel,
1602
+ });
1603
+ const result = await generateImage(body, config);
1604
+ const status = result.ok ? 200 : result.blocked ? 403 : 400;
1605
+ res.status(status).json(result);
1606
+ } catch (error) {
1607
+ res.status(400).json({ ok: false, toolName: "generate_image", error: error instanceof Error ? error.message : String(error) });
1608
+ }
1609
+ });
1610
+
1588
1611
  app.post("/api/keys/:provider", async (req, res) => {
1589
1612
  try {
1590
1613
  const result = await setProviderKey(baseDir, req.params.provider, req.body?.apiKey || req.body?.key || "");