dsh-image-gen 0.9.1 → 0.9.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/lib/index.js CHANGED
@@ -5604,7 +5604,7 @@ function configuredField(extra) {
5604
5604
  }
5605
5605
  /** Model names alone do not establish the protocol of an unknown relay. */
5606
5606
  function officialContract(config, mode) {
5607
- const model = (mode === "generate" ? config.openaiCompatExtraBody : config.openaiCompatEditExtra)?.model ?? config.openaiCompatModel;
5607
+ const model = (mode === "generate" ? config.openaiCompatExtraBody : config.openaiCompatEditExtra)?.model ?? (mode === "edit" ? config.openaiCompatEditModel?.trim() || config.openaiCompatModel : config.openaiCompatModel);
5608
5608
  if (typeof model !== "string") return void 0;
5609
5609
  try {
5610
5610
  const url = new URL(config.openaiCompatBaseURL ?? "");
@@ -5616,6 +5616,10 @@ function officialContract(config, mode) {
5616
5616
  }
5617
5617
  /** Known official edit contracts take precedence over relay-specific format settings. */
5618
5618
  function compatEditFormat(config) {
5619
+ try {
5620
+ const url = new URL(config.openaiCompatBaseURL ?? "");
5621
+ if (url.protocol === "https:" && !url.port && !url.search && !url.hash && (url.hostname === "api.siliconflow.cn" || url.hostname === "api.siliconflow.com") && url.pathname.replace(/\/+$/, "") === "/v1") return "siliconflowJson";
5622
+ } catch {}
5619
5623
  const contract = officialContract(config, "edit");
5620
5624
  if (contract === "sensenova") return "jsonImageUrlArray";
5621
5625
  if (contract === "qwen") return "qwenJson";
@@ -5827,6 +5831,10 @@ const STUDIO_WATERMARK_VALUES = [
5827
5831
  "off",
5828
5832
  "on"
5829
5833
  ];
5834
+ /** A model picker/request must use the same operation-specific default. */
5835
+ function studioModelForMode(profile, mode) {
5836
+ return mode === "edit" ? profile.editModel ?? profile.model : profile.model;
5837
+ }
5830
5838
  function studioWatermarkCapability(profile, mode) {
5831
5839
  return profile?.watermarkCapabilities?.[mode] ?? (mode === "generate" && profile?.watermarkOptions?.length ? { status: "supported" } : {
5832
5840
  status: "unknown",
@@ -5955,11 +5963,13 @@ const Config = z.object({
5955
5963
  provider: volatile(z.union(IMAGE_PROVIDERS).default("google")),
5956
5964
  googleModel: volatile(z.string().default(DEFAULT_GOOGLE_MODEL)),
5957
5965
  googleEndpoint: volatile(z.string().default(DEFAULT_GOOGLE_ENDPOINT)),
5966
+ googleNative: volatile(z.boolean().default(false)),
5958
5967
  openaiBaseURL: volatile(z.string().default(DEFAULT_OPENAI_BASE_URL)),
5959
5968
  openaiModel: volatile(z.string().default(DEFAULT_OPENAI_MODEL)),
5960
5969
  openaiSizes: volatile(z.dict(z.dict(z.string())).default({})),
5961
5970
  openaiCompatBaseURL: volatile(z.string().default("")),
5962
5971
  openaiCompatModel: volatile(z.string().default("")),
5972
+ openaiCompatEditModel: volatile(z.string().default("")),
5963
5973
  openaiCompatQuality: volatile(z.string().default("")),
5964
5974
  openaiCompatQualityOptions: volatile(z.array(z.string()).default([
5965
5975
  "auto",
@@ -5972,7 +5982,8 @@ const Config = z.object({
5972
5982
  openaiCompatEditFormat: volatile(z.union([
5973
5983
  z.const("multipart"),
5974
5984
  z.const("jsonImageUrlArray"),
5975
- z.const("formReferenceImages")
5985
+ z.const("formReferenceImages"),
5986
+ z.const("siliconflowJson")
5976
5987
  ]).default("multipart")),
5977
5988
  openaiCompatEditExtra: volatile(z.dict(z.any()).default({})),
5978
5989
  openaiCompatSizes: volatile(z.dict(z.dict(z.string())).default({})),
@@ -6090,6 +6101,7 @@ function resolveProvider(config) {
6090
6101
  apiKeyEnv: GOOGLE_API_KEY_ENV,
6091
6102
  model: config.googleModel ?? "gemini-3.1-flash-image",
6092
6103
  endpoint: config.googleEndpoint ?? "https://generativelanguage.googleapis.com/v1beta/interactions",
6104
+ native: config.googleNative === true,
6093
6105
  aspectRatio: "1:1",
6094
6106
  imageSize: "1K"
6095
6107
  };
@@ -6099,13 +6111,12 @@ function resolveProvider(config) {
6099
6111
  * Apply a per-call provider and/or model override on top of the saved config.
6100
6112
  * `model` is ignored for ComfyUI, whose per-call equivalent is `workflow`.
6101
6113
  */
6102
- function withProviderOverrides(config, provider, model) {
6114
+ function withProviderOverrides(config, provider, model, mode = "generate") {
6103
6115
  const base = provider === void 0 ? { ...config } : {
6104
6116
  ...config,
6105
6117
  provider
6106
6118
  };
6107
- if (model === void 0) return base;
6108
- const trimmed = model.trim();
6119
+ const trimmed = model?.trim() || (mode === "edit" && base.provider === "openai-compat" ? base.openaiCompatEditModel?.trim() : "") || "";
6109
6120
  if (trimmed.length === 0) return base;
6110
6121
  switch (base.provider ?? "google") {
6111
6122
  case "google": return {
@@ -6118,7 +6129,8 @@ function withProviderOverrides(config, provider, model) {
6118
6129
  };
6119
6130
  case "openai-compat": return {
6120
6131
  ...base,
6121
- openaiCompatModel: trimmed
6132
+ openaiCompatModel: trimmed,
6133
+ ...mode === "edit" ? { openaiCompatEditModel: trimmed } : {}
6122
6134
  };
6123
6135
  case "seedream": return {
6124
6136
  ...base,
@@ -7563,12 +7575,13 @@ function generateGoogleImage(input) {
7563
7575
  return requestGoogleImage({
7564
7576
  ...input,
7565
7577
  operation: "generation",
7566
- interactionInput: input.prompt
7578
+ interactionInput: input.prompt,
7579
+ nativeParts: [{ text: input.prompt }]
7567
7580
  });
7568
7581
  }
7569
7582
  /** Send one native Google image-editing request using already-resolved bytes. */
7570
7583
  function editGoogleImage(input) {
7571
- const interactionInput = input.sourceImages.length === 1 ? [{
7584
+ const interactionInput = input.native ? [] : input.sourceImages.length === 1 ? [{
7572
7585
  type: "image",
7573
7586
  mime_type: input.sourceImages[0].mediaType,
7574
7587
  data: Buffer.from(input.sourceImages[0].data).toString("base64")
@@ -7589,13 +7602,41 @@ function editGoogleImage(input) {
7589
7602
  return requestGoogleImage({
7590
7603
  ...input,
7591
7604
  operation: "editing",
7592
- interactionInput
7605
+ interactionInput,
7606
+ nativeParts: input.native ? [...input.sourceImages.map((image) => ({ inlineData: {
7607
+ mimeType: image.mediaType,
7608
+ data: Buffer.from(image.data).toString("base64")
7609
+ } })), { text: input.prompt }] : []
7593
7610
  });
7594
7611
  }
7595
7612
  /** Shared Google request, response parsing, decoding, and size enforcement. */
7596
7613
  async function requestGoogleImage(input) {
7597
7614
  const label = `Google image ${input.operation}`;
7598
- const response = await guardedFetch(`${label} request`, input.signal, () => fetch(input.endpoint, {
7615
+ const endpoint = input.native ? nativeGoogleUrl(input.endpoint, input.model).toString() : input.endpoint;
7616
+ const body = input.native ? {
7617
+ contents: [{
7618
+ role: "user",
7619
+ parts: input.nativeParts
7620
+ }],
7621
+ generationConfig: {
7622
+ candidateCount: 1,
7623
+ responseModalities: ["TEXT", "IMAGE"],
7624
+ imageConfig: {
7625
+ aspectRatio: input.aspectRatio,
7626
+ imageSize: input.imageSize
7627
+ }
7628
+ }
7629
+ } : {
7630
+ model: input.model,
7631
+ input: input.interactionInput,
7632
+ response_format: {
7633
+ type: "image",
7634
+ mime_type: REQUESTED_MEDIA_TYPE,
7635
+ aspect_ratio: input.aspectRatio,
7636
+ image_size: input.imageSize
7637
+ }
7638
+ };
7639
+ const response = await guardedFetch(`${label} request`, input.signal, () => fetch(endpoint, {
7599
7640
  method: "POST",
7600
7641
  redirect: "error",
7601
7642
  signal: input.signal,
@@ -7603,16 +7644,7 @@ async function requestGoogleImage(input) {
7603
7644
  "content-type": "application/json",
7604
7645
  "x-goog-api-key": input.apiKey
7605
7646
  },
7606
- body: JSON.stringify({
7607
- model: input.model,
7608
- input: input.interactionInput,
7609
- response_format: {
7610
- type: "image",
7611
- mime_type: REQUESTED_MEDIA_TYPE,
7612
- aspect_ratio: input.aspectRatio,
7613
- image_size: input.imageSize
7614
- }
7615
- })
7647
+ body: JSON.stringify(body)
7616
7648
  }), input.apiKey);
7617
7649
  const text = await readBoundedText$2(response, Math.ceil(input.maxBytes * 1.4) + ERROR_LIMIT$2, label);
7618
7650
  if (!response.ok) throw new Error(`${label} failed (${response.status}): ${redactSecrets(text, input.apiKey).slice(0, ERROR_LIMIT$2)}`);
@@ -7622,7 +7654,7 @@ async function requestGoogleImage(input) {
7622
7654
  } catch {
7623
7655
  throw new Error(`${label} returned invalid JSON`);
7624
7656
  }
7625
- const image = outputImage(payload);
7657
+ const image = input.native ? nativeOutputImage(payload) : outputImage(payload);
7626
7658
  if (image === void 0) throw new Error(`${label} returned no image: ${redactSecrets(text, input.apiKey).slice(0, ERROR_LIMIT$2)}`);
7627
7659
  const data = decodeBase64$2(image.data, label);
7628
7660
  if (data.byteLength > input.maxBytes) throw new Error(`${label} exceeded the ${String(input.maxBytes)} byte image limit`);
@@ -7633,6 +7665,49 @@ async function requestGoogleImage(input) {
7633
7665
  mediaType
7634
7666
  };
7635
7667
  }
7668
+ /** Accept a versioned base, the existing interactions URL, or a full native URL. */
7669
+ function nativeGoogleUrl(endpoint, model) {
7670
+ let url;
7671
+ try {
7672
+ url = new URL(endpoint);
7673
+ } catch {
7674
+ throw new Error("Google endpoint must be an absolute URL");
7675
+ }
7676
+ const base = url.pathname.replace(/\/+$/, "").replace(/\/(?:interactions|models(?:\/[^/]+:generateContent)?)$/, "");
7677
+ url.pathname = `${base}/models${model === void 0 ? "" : `/${encodeURIComponent(model.replace(/^models\//, ""))}:generateContent`}`;
7678
+ return url;
7679
+ }
7680
+ /** The settings probe and model picker use the same base as image requests. */
7681
+ function googleModelsEndpoint(endpoint, native) {
7682
+ if (native) return nativeGoogleUrl(endpoint).toString();
7683
+ let url;
7684
+ try {
7685
+ url = new URL(endpoint);
7686
+ } catch {
7687
+ throw new Error("Google endpoint must be an absolute URL");
7688
+ }
7689
+ const segments = url.pathname.split("/").filter(Boolean);
7690
+ segments[segments.length - 1] = "models";
7691
+ url.pathname = `/${segments.join("/")}`;
7692
+ return url.toString();
7693
+ }
7694
+ function nativeOutputImage(value) {
7695
+ const candidates = record$5(value)?.candidates;
7696
+ if (!Array.isArray(candidates)) return void 0;
7697
+ for (const candidate of candidates) {
7698
+ const parts = record$5(record$5(candidate)?.content)?.parts;
7699
+ if (!Array.isArray(parts)) continue;
7700
+ for (const value of parts) {
7701
+ const part = record$5(value);
7702
+ if (part?.thought === true) continue;
7703
+ const image = record$5(part?.inlineData ?? part?.inline_data);
7704
+ if (typeof image?.data === "string") return {
7705
+ data: image.data,
7706
+ mime_type: image.mimeType ?? image.mime_type
7707
+ };
7708
+ }
7709
+ }
7710
+ }
7636
7711
  function outputImage(value) {
7637
7712
  const interaction = record$5(value);
7638
7713
  if (interaction === void 0) return void 0;
@@ -8198,6 +8273,39 @@ async function generateOpenAICompatibleImage(input) {
8198
8273
  }), input.apiKey), input.provider, input);
8199
8274
  }
8200
8275
  async function editOpenAICompatibleImage(input) {
8276
+ if (input.editFormat === "siliconflowJson") {
8277
+ const body = {
8278
+ model: input.model,
8279
+ prompt: input.prompt,
8280
+ ...input.editExtra ?? {},
8281
+ ...input.extraBody ?? {}
8282
+ };
8283
+ const model = typeof body.model === "string" ? body.model.trim() : "";
8284
+ if (!model) throw new Error("硅基流动图生图需要编辑模型");
8285
+ body.model = model;
8286
+ if (model === "Qwen/Qwen-Image") throw new Error("Qwen/Qwen-Image 是文生图模型;请配置图生图模型,例如 Qwen/Qwen-Image-Edit");
8287
+ const maxReferences = model === "Qwen/Qwen-Image-Edit-2509" ? 3 : 1;
8288
+ if (input.sourceImages.length < 1 || input.sourceImages.length > maxReferences) throw new Error(`${model} 图生图支持 1–${String(maxReferences)} 张参考图,当前 ${String(input.sourceImages.length)} 张;请精简参考图或选择支持多图的编辑模型`);
8289
+ if ([
8290
+ "size",
8291
+ "image_size",
8292
+ "quality"
8293
+ ].some((key) => key in body)) throw new Error("此硅基流动图生图格式不发送 size、image_size 或 quality;请从图生图额外字段中移除这些字段");
8294
+ delete body.image;
8295
+ delete body.image2;
8296
+ delete body.image3;
8297
+ for (const [index, source] of input.sourceImages.entries()) body[index === 0 ? "image" : `image${String(index + 1)}`] = toDataUrl$2(source);
8298
+ return parseImageResponse(await guardedFetch("siliconflow image edit request", input.signal, () => fetch(imageEndpoint$1(input.baseURL, "generations"), {
8299
+ method: "POST",
8300
+ redirect: "error",
8301
+ signal: input.signal,
8302
+ headers: {
8303
+ authorization: `Bearer ${input.apiKey}`,
8304
+ "content-type": "application/json"
8305
+ },
8306
+ body: JSON.stringify(body)
8307
+ }), input.apiKey), "siliconflow", input);
8308
+ }
8201
8309
  if (input.editFormat === "qwenJson") return parseImageResponse(await guardedFetch("openai-compat image edit request", input.signal, () => fetch(imageEndpoint$1(input.baseURL, "generations"), {
8202
8310
  method: "POST",
8203
8311
  redirect: "error",
@@ -10093,16 +10201,18 @@ function jsonError$3(res, status, code) {
10093
10201
  //#region lib/types/subscription.js
10094
10202
  /** Translate image tool options into each subscription channel's wire inputs. */
10095
10203
  function subscriptionToolParameters(provider, options) {
10096
- if (options.size !== void 0 && options.aspectRatio !== void 0) throw new Error("size 与 aspect_ratio 请只选一个");
10097
- const ratio = options.aspectRatio;
10204
+ const requestedSize = options.size?.trim() || void 0;
10205
+ const ratio = options.aspectRatio?.trim() || void 0;
10206
+ const imageSize = options.imageSize?.trim() || void 0;
10207
+ if (requestedSize !== void 0 && ratio !== void 0) throw new Error("size 与 aspect_ratio 请只选一个;无关可选字段请省略,不要填写空字符串");
10098
10208
  if (ratio !== void 0 && !SUBSCRIPTION_CAPABILITIES[provider].ratioOptions.some((option) => option.value === ratio)) throw new Error(`${provider} 不支持比例 ${ratio}`);
10099
- const size = options.size ?? (ratio === void 0 ? void 0 : provider === "chatgpt-sub" ? SUBSCRIPTION_CAPABILITIES[provider].sizeFor?.(ratio, "auto") : ratio);
10209
+ const size = requestedSize ?? (ratio === void 0 ? void 0 : provider === "chatgpt-sub" ? SUBSCRIPTION_CAPABILITIES[provider].sizeFor?.(ratio, "auto") : ratio);
10100
10210
  let quality;
10101
- if (options.imageSize !== void 0) {
10102
- if (provider === "grok-sub" && (options.imageSize === "1K" || options.imageSize === "2K")) quality = options.imageSize.toLowerCase();
10103
- else if (provider === "google-sub" && options.imageSize === "4K") quality = "hd";
10104
- else if (provider === "google-sub" && options.imageSize === "1K") quality = void 0;
10105
- else throw new Error(`${provider} 不支持清晰度 ${options.imageSize}`);
10211
+ if (imageSize !== void 0) {
10212
+ if (provider === "grok-sub" && (imageSize === "1K" || imageSize === "2K")) quality = imageSize.toLowerCase();
10213
+ else if (provider === "google-sub" && imageSize === "4K") quality = "hd";
10214
+ else if (provider === "google-sub" && imageSize === "1K") quality = void 0;
10215
+ else throw new Error(`${provider} 不支持清晰度 ${imageSize};${provider === "chatgpt-sub" ? "请省略 image_size,使用 size 或 aspect_ratio 指定尺寸" : `image_size 仅支持 ${provider === "grok-sub" ? "1K、2K" : "1K、4K"}`}`);
10106
10216
  }
10107
10217
  return {
10108
10218
  ...size === void 0 ? {} : { size },
@@ -27771,6 +27881,7 @@ async function generateFromStudio(ctx, config, input, signal, fallbackWorkspaceR
27771
27881
  generated = input.mode === "edit" ? await editGoogleImage({
27772
27882
  apiKey: credential,
27773
27883
  endpoint: wired.endpoint,
27884
+ native: wired.native,
27774
27885
  model: wired.model,
27775
27886
  prompt: input.prompt,
27776
27887
  sourceImages,
@@ -27781,6 +27892,7 @@ async function generateFromStudio(ctx, config, input, signal, fallbackWorkspaceR
27781
27892
  }) : await generateGoogleImage({
27782
27893
  apiKey: credential,
27783
27894
  endpoint: wired.endpoint,
27895
+ native: wired.native,
27784
27896
  model: wired.model,
27785
27897
  prompt: input.prompt,
27786
27898
  aspectRatio,
@@ -28055,8 +28167,10 @@ function subscriptionResponse(input, startedAt, requestedCount, failedCount, ite
28055
28167
  }
28056
28168
  function studioProfile(config, provider, configured) {
28057
28169
  const result = cloudStudioProfile(config, provider, configured);
28170
+ const editModel = provider === "openai-compat" ? config.openaiCompatEditModel?.trim() : void 0;
28058
28171
  return {
28059
28172
  ...result,
28173
+ ...editModel ? { editModel } : {},
28060
28174
  ...watermarkProfile(config, provider, result.model)
28061
28175
  };
28062
28176
  }
@@ -28154,7 +28268,7 @@ function option(value) {
28154
28268
  };
28155
28269
  }
28156
28270
  function assertAllowed(profile, input) {
28157
- if (input.model !== profile.model) throw new Error("模型配置已变化,请刷新工作台后重试");
28271
+ if (input.model !== studioModelForMode(profile, input.mode)) throw new Error("模型配置已变化,请刷新工作台后重试");
28158
28272
  if (!profile.ratioOptions.some((option) => option.value === input.ratio)) throw new Error("该 Provider 不支持所选比例");
28159
28273
  const selectedQuality = input.provider === "openai" ? input.generationQuality ?? input.quality : input.resolution ?? input.quality;
28160
28274
  if (!profile.qualityOptions.some((option) => option.value === selectedQuality)) throw new Error("该 Provider 不支持所选清晰度");
@@ -28413,15 +28527,7 @@ function googleModelsUrl(config) {
28413
28527
  const endpoint = config.googleEndpoint;
28414
28528
  const fallback = "https://generativelanguage.googleapis.com/v1beta/models";
28415
28529
  if (typeof endpoint !== "string" || endpoint.trim().length === 0) return fallback;
28416
- try {
28417
- const url = new URL(endpoint);
28418
- const segments = url.pathname.split("/").filter((segment) => segment.length > 0);
28419
- segments[segments.length - 1] = "models";
28420
- url.pathname = `/${segments.join("/")}`;
28421
- return url.toString();
28422
- } catch {
28423
- throw new Error("Google endpoint must be an absolute URL");
28424
- }
28530
+ return googleModelsEndpoint(endpoint, config.googleNative === true);
28425
28531
  }
28426
28532
  function joinUrl(base, path) {
28427
28533
  return new URL(path, base.endsWith("/") ? base : `${base}/`).toString();
@@ -28732,14 +28838,53 @@ function serveSettingsHealth(req, res, health) {
28732
28838
  res.end(JSON.stringify(health));
28733
28839
  }
28734
28840
  //#endregion
28841
+ //#region lib/types/image-tool-context.js
28842
+ /** Live, credential-free instructions for the conversation model's image tools. */
28843
+ function imageToolContext(config) {
28844
+ const provider = config.provider ?? "google";
28845
+ let model = "not configured";
28846
+ let editModel = model;
28847
+ try {
28848
+ const active = resolveProvider(config);
28849
+ model = active.provider === "comfyui" ? active.workflow?.name ?? "workflow selected in settings" : active.model;
28850
+ const edit = resolveProvider(withProviderOverrides(config, void 0, void 0, "edit"));
28851
+ editModel = edit.provider === "comfyui" ? edit.workflow?.name ?? model : edit.model;
28852
+ } catch {}
28853
+ return [
28854
+ `Image tools current default provider: ${provider}; generation model: ${JSON.stringify(model)}; editing model: ${JSON.stringify(editModel)}.`,
28855
+ "Unless the user explicitly requests a different image provider or model, OMIT provider and model from generate_image, generate_images and edit_image. Do not guess a channel or switch channels after an error.",
28856
+ "Omit unused optional arguments entirely; never fill them with empty strings. size and aspect_ratio are alternatives: provide at most one.",
28857
+ "Subscription parameters: chatgpt-sub accepts size OR aspect_ratio, but NOT image_size. grok-sub image_size: 1K or 2K. google-sub image_size: 1K or 4K. Do not copy parameters from another provider.",
28858
+ "Minimal calls: generate_image({\"prompt\":\"a sailboat\"}); edit_image({\"prompt\":\"make the sail blue\"}). A valid ChatGPT subscription ratio example is {\"prompt\":\"a sailboat\",\"aspect_ratio\":\"1:1\"} with image_size and size omitted."
28859
+ ].join("\n");
28860
+ }
28861
+ //#endregion
28735
28862
  //#region lib/types/index.js
28736
28863
  const name = "dsh-image-gen";
28737
28864
  const inject = [
28738
28865
  "tools",
28739
28866
  "attachments",
28740
- "credentials",
28741
- "webServer"
28867
+ "credentials"
28742
28868
  ];
28869
+ /** Optional strings from tool callers may contain empty placeholders (#86). */
28870
+ function cleanImageToolArgs(args) {
28871
+ const clean = { ...args };
28872
+ const fields = clean;
28873
+ for (const key of [
28874
+ "model",
28875
+ "size",
28876
+ "aspect_ratio",
28877
+ "image_size",
28878
+ "quality",
28879
+ "workflow"
28880
+ ]) {
28881
+ const value = fields[key];
28882
+ if (typeof value !== "string") continue;
28883
+ if (value.trim().length === 0) delete fields[key];
28884
+ else fields[key] = value.trim();
28885
+ }
28886
+ return clean;
28887
+ }
28743
28888
  /** Validate the untrusted per-call provider override from tool arguments. */
28744
28889
  function providerOverrideOf(value) {
28745
28890
  if (value === void 0 || value === null || value === "") return void 0;
@@ -28756,142 +28901,152 @@ function apply(ctx, config = {}) {
28756
28901
  const knownWorkspaceRoots = /* @__PURE__ */ new Set();
28757
28902
  const canvasMirror = new CanvasMirror();
28758
28903
  const subscriptionManager = new SubscriptionManager(ctx);
28759
- registerSubscriptionRoutes(ctx, subscriptionManager);
28760
28904
  const settingsHealth = installImageSettings(ctx, current(), {
28761
28905
  setSource: (source) => {
28762
28906
  current = () => migrateOpenAICompatConfig(plainConfig(source()));
28763
28907
  },
28764
28908
  onChange: () => {}
28765
28909
  });
28766
- ctx.effect(() => ctx.webServer.register({
28767
- kind: "exact",
28768
- path: SETTINGS_HEALTH_ROUTE,
28769
- handler: (req, res) => serveSettingsHealth(req, res, settingsHealth())
28770
- }), "dsh-image-gen: settings health");
28771
- ctx.effect(() => ctx.webServer.register({
28772
- kind: "exact",
28773
- path: IMAGE_ROUTE,
28774
- handler: (req, res) => serveImage(req, res, { readImage: (ref) => ctx.attachments.readImage(ref) })
28775
- }), "dsh-image-gen: image route");
28776
- ctx.effect(() => ctx.webServer.register({
28777
- kind: "exact",
28778
- path: IMPORT_ROUTE,
28779
- handler: (req, res) => serveImport(req, res, {
28780
- saveImage: (image) => ctx.attachments.saveImage(image),
28781
- maxImageBytes: ctx.attachments.imageLimits.maxImageBytes,
28782
- mediaTypes: ctx.attachments.imageLimits.mediaTypes
28783
- })
28784
- }), "dsh-image-gen: import route");
28785
- ctx.effect(() => ctx.webServer.register({
28786
- kind: "exact",
28787
- path: DELETE_ROUTE,
28788
- handler: (req, res) => serveDelete(req, res, { deleteWorkspaceImage: async (filePath) => {
28789
- const discovered = await getDshWorkspaceRoots().catch(() => []);
28790
- return deleteImageFromWorkspace(filePath, /* @__PURE__ */ new Set([
28791
- ...knownWorkspaceRoots,
28792
- ...discovered,
28793
- process.cwd()
28794
- ]));
28795
- } })
28796
- }), "dsh-image-gen: delete route");
28797
- ctx.effect(() => ctx.webServer.register({
28798
- kind: "exact",
28799
- path: SAVE_WORKSPACE_ROUTE,
28800
- handler: (req, res) => serveSaveWorkspace(req, res, {
28801
- readImage: (ref) => ctx.attachments.readImage(ref),
28802
- saveToWorkspace: (options) => {
28803
- if (options.workspaceRoot) knownWorkspaceRoots.add(options.workspaceRoot);
28804
- return saveImageToWorkspace({
28805
- workspaceRoot: options.workspaceRoot,
28806
- folder: current().workspaceFolder,
28807
- attachmentId: options.attachmentId,
28808
- mediaType: options.mediaType,
28809
- data: options.data
28810
- });
28811
- },
28812
- getActiveWorkspaceRoot: () => Array.from(knownWorkspaceRoots)[0] || process.cwd(),
28813
- getAllowedWorkspaceRoots: async () => {
28910
+ ctx.inject(["webServer"], (webCtx) => {
28911
+ registerSubscriptionRoutes(webCtx, subscriptionManager);
28912
+ webCtx.effect(() => webCtx.webServer.register({
28913
+ kind: "exact",
28914
+ path: SETTINGS_HEALTH_ROUTE,
28915
+ handler: (req, res) => serveSettingsHealth(req, res, settingsHealth())
28916
+ }), "dsh-image-gen: settings health");
28917
+ webCtx.effect(() => webCtx.webServer.register({
28918
+ kind: "exact",
28919
+ path: IMAGE_ROUTE,
28920
+ handler: (req, res) => serveImage(req, res, { readImage: (ref) => ctx.attachments.readImage(ref) })
28921
+ }), "dsh-image-gen: image route");
28922
+ webCtx.effect(() => webCtx.webServer.register({
28923
+ kind: "exact",
28924
+ path: IMPORT_ROUTE,
28925
+ handler: (req, res) => serveImport(req, res, {
28926
+ saveImage: (image) => ctx.attachments.saveImage(image),
28927
+ maxImageBytes: ctx.attachments.imageLimits.maxImageBytes,
28928
+ mediaTypes: ctx.attachments.imageLimits.mediaTypes
28929
+ })
28930
+ }), "dsh-image-gen: import route");
28931
+ webCtx.effect(() => webCtx.webServer.register({
28932
+ kind: "exact",
28933
+ path: DELETE_ROUTE,
28934
+ handler: (req, res) => serveDelete(req, res, { deleteWorkspaceImage: async (filePath) => {
28814
28935
  const discovered = await getDshWorkspaceRoots().catch(() => []);
28815
- return /* @__PURE__ */ new Set([
28936
+ return deleteImageFromWorkspace(filePath, /* @__PURE__ */ new Set([
28816
28937
  ...knownWorkspaceRoots,
28817
28938
  ...discovered,
28818
28939
  process.cwd()
28819
- ]);
28820
- },
28821
- isSaveEnabled: () => current().saveToWorkspace !== false
28822
- })
28823
- }), "dsh-image-gen: save workspace route");
28824
- ctx.effect(() => ctx.webServer.register({
28825
- kind: "exact",
28826
- path: TEST_CONNECTION_ROUTE,
28827
- handler: (req, res) => serveTestConnection(req, res, {
28828
- resolveKey: (provider) => resolveApiKey(ctx, provider),
28829
- config: () => current(),
28830
- subscriptionManager
28831
- })
28832
- }), "dsh-image-gen: test connection route");
28833
- ctx.effect(() => ctx.webServer.register({
28834
- kind: "exact",
28835
- path: CANVAS_STATE_ROUTE,
28836
- handler: (req, res) => serveCanvasState(req, res, {
28837
- mirror: canvasMirror,
28838
- maxBodyBytes: Math.ceil(ctx.attachments.imageLimits.maxImageBytes * 1.4) + 262144,
28839
- maxImageBytes: ctx.attachments.imageLimits.maxImageBytes
28840
- })
28841
- }), "dsh-image-gen: canvas state route");
28842
- ctx.effect(() => ctx.webServer.register({
28843
- kind: "exact",
28844
- path: CANVAS_ASSET_ROUTE,
28845
- handler: (req, res) => serveCanvasAsset(req, res, {
28846
- maxImageBytes: ctx.attachments.imageLimits.maxImageBytes,
28847
- saveImage: (image) => ctx.attachments.saveImage(image)
28848
- })
28849
- }), "dsh-image-gen: canvas asset route");
28940
+ ]));
28941
+ } })
28942
+ }), "dsh-image-gen: delete route");
28943
+ webCtx.effect(() => webCtx.webServer.register({
28944
+ kind: "exact",
28945
+ path: SAVE_WORKSPACE_ROUTE,
28946
+ handler: (req, res) => serveSaveWorkspace(req, res, {
28947
+ readImage: (ref) => ctx.attachments.readImage(ref),
28948
+ saveToWorkspace: (options) => {
28949
+ if (options.workspaceRoot) knownWorkspaceRoots.add(options.workspaceRoot);
28950
+ return saveImageToWorkspace({
28951
+ workspaceRoot: options.workspaceRoot,
28952
+ folder: current().workspaceFolder,
28953
+ attachmentId: options.attachmentId,
28954
+ mediaType: options.mediaType,
28955
+ data: options.data
28956
+ });
28957
+ },
28958
+ getActiveWorkspaceRoot: () => Array.from(knownWorkspaceRoots)[0] || process.cwd(),
28959
+ getAllowedWorkspaceRoots: async () => {
28960
+ const discovered = await getDshWorkspaceRoots().catch(() => []);
28961
+ return /* @__PURE__ */ new Set([
28962
+ ...knownWorkspaceRoots,
28963
+ ...discovered,
28964
+ process.cwd()
28965
+ ]);
28966
+ },
28967
+ isSaveEnabled: () => current().saveToWorkspace !== false
28968
+ })
28969
+ }), "dsh-image-gen: save workspace route");
28970
+ webCtx.effect(() => webCtx.webServer.register({
28971
+ kind: "exact",
28972
+ path: TEST_CONNECTION_ROUTE,
28973
+ handler: (req, res) => serveTestConnection(req, res, {
28974
+ resolveKey: (provider) => resolveApiKey(ctx, provider),
28975
+ config: () => current(),
28976
+ subscriptionManager
28977
+ })
28978
+ }), "dsh-image-gen: test connection route");
28979
+ webCtx.effect(() => webCtx.webServer.register({
28980
+ kind: "exact",
28981
+ path: CANVAS_STATE_ROUTE,
28982
+ handler: (req, res) => serveCanvasState(req, res, {
28983
+ mirror: canvasMirror,
28984
+ maxBodyBytes: Math.ceil(ctx.attachments.imageLimits.maxImageBytes * 1.4) + 262144,
28985
+ maxImageBytes: ctx.attachments.imageLimits.maxImageBytes
28986
+ })
28987
+ }), "dsh-image-gen: canvas state route");
28988
+ webCtx.effect(() => webCtx.webServer.register({
28989
+ kind: "exact",
28990
+ path: CANVAS_ASSET_ROUTE,
28991
+ handler: (req, res) => serveCanvasAsset(req, res, {
28992
+ maxImageBytes: ctx.attachments.imageLimits.maxImageBytes,
28993
+ saveImage: (image) => ctx.attachments.saveImage(image)
28994
+ })
28995
+ }), "dsh-image-gen: canvas asset route");
28996
+ });
28850
28997
  ctx.inject(["systemPrompt"], (promptCtx) => {
28851
- promptCtx.systemPrompt.context({
28998
+ promptCtx.effect(() => promptCtx.systemPrompt.context({
28999
+ name: "dsh-image-gen:image-defaults",
29000
+ order: 59,
29001
+ text: () => imageToolContext(current())
29002
+ }), "dsh-image-gen: image tool context");
29003
+ promptCtx.effect(() => promptCtx.systemPrompt.context({
28852
29004
  name: "dsh-image-gen:canvas",
28853
29005
  order: 60,
28854
29006
  text: () => canvasMirror.digest()
28855
- });
29007
+ }), "dsh-image-gen: canvas context");
28856
29008
  });
28857
29009
  registerCanvasTools(ctx, canvasMirror, { persistSelectionImage: (image) => ctx.attachments.saveImage({
28858
29010
  data: image.data,
28859
29011
  mediaType: image.mediaType,
28860
29012
  name: "canvas-selection"
28861
29013
  }) });
28862
- ctx.effect(() => ctx.webServer.register({
28863
- kind: "exact",
28864
- path: STUDIO_ROUTE,
28865
- handler: (req, res) => serveStudio(req, res, {
28866
- describe: async () => {
28867
- const base = await describeStudio(ctx, current(), subscriptionManager);
28868
- const workspaces = await getDshWorkspacesFull().catch(() => []);
28869
- const activeRoot = Array.from(knownWorkspaceRoots)[0] || workspaces[0]?.path || process.cwd();
28870
- return {
28871
- ...base,
28872
- workspaceRoot: activeRoot,
28873
- workspaces
28874
- };
28875
- },
28876
- generate: (input, signal) => {
28877
- const fallbackRoot = Array.from(knownWorkspaceRoots)[0] || process.cwd();
28878
- return generateFromStudio(ctx, current(), input, signal, fallbackRoot, subscriptionManager);
28879
- },
28880
- maxBodyBytes: Math.ceil(ctx.attachments.imageLimits.maxImageBytes * 1.4 * 5) + 262144
28881
- })
28882
- }), "dsh-image-gen: studio route");
28883
- const serveInspiration = createInspirationRoute();
28884
- ctx.effect(() => ctx.webServer.register({
28885
- kind: "prefix",
28886
- path: INSPIRATION_ROUTE,
28887
- handler: (req, res) => {
28888
- const originalUrl = req.url ?? "/";
28889
- req.url = originalUrl.startsWith("/plugins/dsh-image-gen/inspiration") ? originalUrl.slice(34) || "/" : originalUrl;
28890
- return serveInspiration(req, res);
28891
- }
28892
- }), "dsh-image-gen: inspiration route");
29014
+ ctx.inject(["webServer"], (webCtx) => {
29015
+ webCtx.effect(() => webCtx.webServer.register({
29016
+ kind: "exact",
29017
+ path: STUDIO_ROUTE,
29018
+ handler: (req, res) => serveStudio(req, res, {
29019
+ describe: async () => {
29020
+ const base = await describeStudio(ctx, current(), subscriptionManager);
29021
+ const workspaces = await getDshWorkspacesFull().catch(() => []);
29022
+ const activeRoot = Array.from(knownWorkspaceRoots)[0] || workspaces[0]?.path || process.cwd();
29023
+ return {
29024
+ ...base,
29025
+ workspaceRoot: activeRoot,
29026
+ workspaces
29027
+ };
29028
+ },
29029
+ generate: (input, signal) => {
29030
+ const fallbackRoot = Array.from(knownWorkspaceRoots)[0] || process.cwd();
29031
+ return generateFromStudio(ctx, current(), input, signal, fallbackRoot, subscriptionManager);
29032
+ },
29033
+ maxBodyBytes: Math.ceil(ctx.attachments.imageLimits.maxImageBytes * 1.4 * 5) + 262144
29034
+ })
29035
+ }), "dsh-image-gen: studio route");
29036
+ const serveInspiration = createInspirationRoute();
29037
+ webCtx.effect(() => webCtx.webServer.register({
29038
+ kind: "prefix",
29039
+ path: INSPIRATION_ROUTE,
29040
+ handler: (req, res) => {
29041
+ const originalUrl = req.url ?? "/";
29042
+ req.url = originalUrl.startsWith("/plugins/dsh-image-gen/inspiration") ? originalUrl.slice(34) || "/" : originalUrl;
29043
+ return serveInspiration(req, res);
29044
+ }
29045
+ }), "dsh-image-gen: inspiration route");
29046
+ });
28893
29047
  /** Per-item generation request shared by generate_image and generate_images. */
28894
- const generateSingle = async (args, exec) => {
29048
+ const generateSingle = async (rawArgs, exec) => {
29049
+ const args = cleanImageToolArgs(rawArgs);
28895
29050
  const active = resolveProvider(withProviderOverrides(current(), providerOverrideOf(args.provider), args.model));
28896
29051
  if (active.provider === "comfyui") {
28897
29052
  const workflow = selectComfyUIWorkflow(active, args.workflow);
@@ -28926,6 +29081,7 @@ function apply(ctx, config = {}) {
28926
29081
  return saveGenerated(ctx, await generateGoogleImage({
28927
29082
  apiKey: credential,
28928
29083
  endpoint: active.endpoint,
29084
+ native: active.native,
28929
29085
  model: active.model,
28930
29086
  prompt: args.prompt,
28931
29087
  aspectRatio,
@@ -29006,7 +29162,7 @@ function apply(ctx, config = {}) {
29006
29162
  "grok-sub",
29007
29163
  "google-sub"
29008
29164
  ],
29009
- description: "Optional provider for this call only (for example when the user asks to use a specific provider); omit to use the configured default. chatgpt-sub, grok-sub, and google-sub generate through the logged-in subscription account instead of an API key."
29165
+ description: "OMIT unless the user explicitly requests a specific image provider. Omission uses the saved default; never guess a channel or fill this with an empty string. chatgpt-sub, grok-sub, and google-sub use logged-in subscription accounts instead of API keys."
29010
29166
  },
29011
29167
  model: {
29012
29168
  type: "string",
@@ -29035,11 +29191,11 @@ function apply(ctx, config = {}) {
29035
29191
  "2K",
29036
29192
  "4K"
29037
29193
  ],
29038
- description: "Optional output resolution for Google Gemini, xAI Grok, Grok subscription (1K/2K), or Google subscription (1K/4K)."
29194
+ description: "Optional output resolution for Google Gemini, xAI Grok, Grok subscription (1K/2K), or Google subscription (1K/4K). NEVER send this for chatgpt-sub; omit unused fields entirely."
29039
29195
  },
29040
29196
  size: {
29041
29197
  type: "string",
29042
- description: "Optional dimensions or size tier for OpenAI, Seedream, or DashScope; WIDTHxHEIGHT maps to aspect_ratio for xAI Grok."
29198
+ description: "Optional dimensions or size tier for OpenAI, Seedream, or DashScope; WIDTHxHEIGHT maps to aspect_ratio for xAI Grok. Do not combine with aspect_ratio; omit instead of using an empty string."
29043
29199
  },
29044
29200
  quality: {
29045
29201
  type: "string",
@@ -29081,7 +29237,7 @@ function apply(ctx, config = {}) {
29081
29237
  "grok-sub",
29082
29238
  "google-sub"
29083
29239
  ],
29084
- description: "Optional provider for this call only, applied to every item; omit to use the configured default."
29240
+ description: "OMIT unless the user explicitly requests a specific image provider. Uses the saved default for all items; never guess a channel or fill unused fields with empty strings."
29085
29241
  },
29086
29242
  model: {
29087
29243
  type: "string",
@@ -29110,7 +29266,7 @@ function apply(ctx, config = {}) {
29110
29266
  "2K",
29111
29267
  "4K"
29112
29268
  ],
29113
- description: "Optional output resolution for Google Gemini, xAI Grok, Grok subscription (1K/2K), or Google subscription (1K/4K)."
29269
+ description: "Optional output resolution for Google Gemini, xAI Grok, Grok subscription (1K/2K), or Google subscription (1K/4K). NEVER send this for chatgpt-sub; omit unused fields entirely."
29114
29270
  },
29115
29271
  size: {
29116
29272
  type: "string",
@@ -29303,7 +29459,7 @@ function apply(ctx, config = {}) {
29303
29459
  "grok-sub",
29304
29460
  "google-sub"
29305
29461
  ],
29306
- description: "Optional provider for this call only (for example when the user asks to use a specific provider); omit to use the configured default. chatgpt-sub, grok-sub, and google-sub edit images through the logged-in subscription account instead of an API key."
29462
+ description: "OMIT unless the user explicitly requests a specific image provider. Omission uses the saved default; never guess a channel or fill this with an empty string. Subscription providers edit through their logged-in accounts."
29307
29463
  },
29308
29464
  model: {
29309
29465
  type: "string",
@@ -29355,7 +29511,7 @@ function apply(ctx, config = {}) {
29355
29511
  "2K",
29356
29512
  "4K"
29357
29513
  ],
29358
- description: "Optional output resolution for Google Gemini, xAI Grok, Grok subscription (1K/2K), or Google subscription (1K/4K)."
29514
+ description: "Optional output resolution for Google Gemini, xAI Grok, Grok subscription (1K/2K), or Google subscription (1K/4K). NEVER send this for chatgpt-sub; omit unused fields entirely."
29359
29515
  },
29360
29516
  size: {
29361
29517
  type: "string",
@@ -29371,8 +29527,9 @@ function apply(ctx, config = {}) {
29371
29527
  }
29372
29528
  },
29373
29529
  output: imageOutput("Edited"),
29374
- async execute(args, exec) {
29375
- const active = resolveProvider(withProviderOverrides(current(), providerOverrideOf(args.provider), args.model));
29530
+ async execute(rawArgs, exec) {
29531
+ const args = cleanImageToolArgs(rawArgs);
29532
+ const active = resolveProvider(withProviderOverrides(current(), providerOverrideOf(args.provider), args.model, "edit"));
29376
29533
  const canvasSelection = args.source === "canvas_selection";
29377
29534
  if (canvasSelection && (args.source_attachment_id !== void 0 || Array.isArray(args.source_attachment_ids) || args.source_path !== void 0 || Array.isArray(args.source_paths))) throw new Error("edit_image source=canvas_selection cannot be combined with source_attachment_id, source_attachment_ids, source_path, or source_paths; provide exactly one selector");
29378
29535
  const sourceImages = canvasSelection ? await resolveCanvasSelectionReferences({
@@ -29433,6 +29590,7 @@ function apply(ctx, config = {}) {
29433
29590
  return saveGenerated(ctx, await editGoogleImage({
29434
29591
  apiKey: credential,
29435
29592
  endpoint: active.endpoint,
29593
+ native: active.native,
29436
29594
  model: active.model,
29437
29595
  prompt: args.prompt,
29438
29596
  sourceImages,