dsh-image-gen 0.9.2 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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";
@@ -5760,12 +5764,80 @@ const CLOUD_IMAGE_PROVIDERS = [
5760
5764
  "xai",
5761
5765
  "zhipu"
5762
5766
  ];
5767
+ /** Normalize the relay model list while keeping the legacy default model selectable. */
5768
+ function resolveOpenAICompatModels(config) {
5769
+ const defaultModel = config.openaiCompatModel?.trim() ?? "";
5770
+ const models = [...new Set((config.openaiCompatModels ?? []).map((model) => model.trim()).filter(Boolean))];
5771
+ if (defaultModel.length > 0 && !models.includes(defaultModel)) models.unshift(defaultModel);
5772
+ return models;
5773
+ }
5774
+ const DEFAULT_OPENAI_COMPAT_CHANNEL_ID = "default";
5775
+ function openAICompatKeyRef(channelId) {
5776
+ return channelId === "default" ? OPENAI_COMPAT_API_KEY_ENV : `${OPENAI_COMPAT_API_KEY_ENV}_${channelId.replace(/[^A-Za-z0-9]/g, "_").toUpperCase()}`;
5777
+ }
5778
+ /** The legacy flat fields remain the first channel; additional entries are fully independent. */
5779
+ function resolveOpenAICompatChannels(source) {
5780
+ const legacy = {
5781
+ id: DEFAULT_OPENAI_COMPAT_CHANNEL_ID,
5782
+ label: source.openaiCompatLabel?.trim() || "OpenAI 兼容",
5783
+ keyRef: OPENAI_COMPAT_API_KEY_ENV,
5784
+ baseURL: source.openaiCompatBaseURL ?? "",
5785
+ model: source.openaiCompatModel ?? "",
5786
+ models: [...source.openaiCompatModels ?? []],
5787
+ editModel: source.openaiCompatEditModel ?? "",
5788
+ quality: source.openaiCompatQuality ?? "",
5789
+ qualityOptions: [...source.openaiCompatQualityOptions ?? []],
5790
+ extraBody: source.openaiCompatExtraBody ?? {},
5791
+ editSizeMode: source.openaiCompatEditSizeMode ?? "auto",
5792
+ editFormat: source.openaiCompatEditFormat ?? "multipart",
5793
+ editExtra: source.openaiCompatEditExtra ?? {},
5794
+ sizes: source.openaiCompatSizes ?? {}
5795
+ };
5796
+ const channels = [legacy];
5797
+ const seen = /* @__PURE__ */ new Set([legacy.id]);
5798
+ for (const [index, entry] of (source.openaiCompatChannels ?? []).entries()) {
5799
+ const id = typeof entry?.id === "string" ? entry.id.trim() : "";
5800
+ if (!/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/.test(id) || seen.has(id)) continue;
5801
+ seen.add(id);
5802
+ const storedKeyRef = typeof entry.keyRef === "string" ? entry.keyRef.trim() : "";
5803
+ channels.push({
5804
+ id,
5805
+ label: typeof entry.label === "string" && entry.label.trim().length > 0 ? entry.label.trim() : `OpenAI 兼容 ${index + 2}`,
5806
+ keyRef: /^DSH_IMAGE_GEN_OPENAI_COMPAT_KEY_[A-Z0-9_]+$/.test(storedKeyRef) ? storedKeyRef : openAICompatKeyRef(id),
5807
+ baseURL: typeof entry.baseURL === "string" ? entry.baseURL : "",
5808
+ model: typeof entry.model === "string" ? entry.model : "",
5809
+ models: Array.isArray(entry.models) ? entry.models.filter((model) => typeof model === "string") : [],
5810
+ editModel: typeof entry.editModel === "string" ? entry.editModel : "",
5811
+ quality: typeof entry.quality === "string" ? entry.quality : "",
5812
+ qualityOptions: Array.isArray(entry.qualityOptions) ? entry.qualityOptions.filter((quality) => typeof quality === "string") : [],
5813
+ extraBody: entry.extraBody ?? {},
5814
+ editSizeMode: entry.editSizeMode === "selected" ? "selected" : "auto",
5815
+ editFormat: entry.editFormat ?? "multipart",
5816
+ editExtra: entry.editExtra ?? {},
5817
+ sizes: entry.sizes ?? {}
5818
+ });
5819
+ }
5820
+ return channels;
5821
+ }
5822
+ function resolveOpenAICompatChannel(source, channelId) {
5823
+ const channels = resolveOpenAICompatChannels(source);
5824
+ const explicit = channelId?.trim() ?? "";
5825
+ if (explicit.length > 0) {
5826
+ const selected = channels.find((channel) => channel.id === explicit);
5827
+ if (selected === void 0) throw new Error(`Unknown OpenAI-compatible channel "${explicit}". Available channels: ${channels.map((channel) => channel.id).join(", ")}.`);
5828
+ return selected;
5829
+ }
5830
+ const active = source.openaiCompatActiveChannel?.trim() ?? "";
5831
+ return channels.find((channel) => channel.id === active) ?? channels[0];
5832
+ }
5763
5833
  /**
5764
5834
  * Providers the browser workbench can drive: the BYOK cloud set plus the
5765
5835
  * logged-in subscription channels. ComfyUI stays out; it has its own workflow
5766
5836
  * pipeline and no shared request shape.
5767
5837
  */
5768
5838
  const STUDIO_PROVIDERS = [...CLOUD_IMAGE_PROVIDERS, ...SUBSCRIPTION_PROVIDERS];
5839
+ /** Default timeout for API-key cloud generation in the browser workbench. */
5840
+ const DEFAULT_CLOUD_TIMEOUT_MS = 15e4;
5769
5841
  /** Timeout for one subscription image call, shared by the tool path and the
5770
5842
  * studio route so neither drifts from the other. */
5771
5843
  const SUBSCRIPTION_TIMEOUT_MS = 3e5;
@@ -5827,6 +5899,16 @@ const STUDIO_WATERMARK_VALUES = [
5827
5899
  "off",
5828
5900
  "on"
5829
5901
  ];
5902
+ function studioModelOptionsForMode(profile, mode) {
5903
+ if (mode === "edit" && profile.editModel !== void 0) return [{
5904
+ value: profile.editModel,
5905
+ label: profile.editModel
5906
+ }];
5907
+ return profile.modelOptions ?? [{
5908
+ value: profile.model,
5909
+ label: profile.model
5910
+ }];
5911
+ }
5830
5912
  function studioWatermarkCapability(profile, mode) {
5831
5913
  return profile?.watermarkCapabilities?.[mode] ?? (mode === "generate" && profile?.watermarkOptions?.length ? { status: "supported" } : {
5832
5914
  status: "unknown",
@@ -5953,14 +6035,40 @@ function volatile(schema) {
5953
6035
  */
5954
6036
  const Config = z.object({
5955
6037
  provider: volatile(z.union(IMAGE_PROVIDERS).default("google")),
6038
+ cloudTimeoutMs: volatile(z.number().min(5e3).max(36e5).default(DEFAULT_CLOUD_TIMEOUT_MS)),
5956
6039
  googleModel: volatile(z.string().default(DEFAULT_GOOGLE_MODEL)),
5957
6040
  googleEndpoint: volatile(z.string().default(DEFAULT_GOOGLE_ENDPOINT)),
5958
6041
  googleNative: volatile(z.boolean().default(false)),
5959
6042
  openaiBaseURL: volatile(z.string().default(DEFAULT_OPENAI_BASE_URL)),
5960
6043
  openaiModel: volatile(z.string().default(DEFAULT_OPENAI_MODEL)),
5961
6044
  openaiSizes: volatile(z.dict(z.dict(z.string())).default({})),
6045
+ openaiCompatLabel: volatile(z.string().default("")),
5962
6046
  openaiCompatBaseURL: volatile(z.string().default("")),
5963
6047
  openaiCompatModel: volatile(z.string().default("")),
6048
+ openaiCompatModels: volatile(z.array(z.string()).default([])),
6049
+ openaiCompatChannels: volatile(z.array(z.object({
6050
+ id: z.string(),
6051
+ label: z.string().default(""),
6052
+ keyRef: z.string(),
6053
+ baseURL: z.string().default(""),
6054
+ model: z.string().default(""),
6055
+ models: z.array(z.string()).default([]),
6056
+ editModel: z.string().default(""),
6057
+ quality: z.string().default(""),
6058
+ qualityOptions: z.array(z.string()).default([]),
6059
+ extraBody: z.dict(z.any()).default({}),
6060
+ editSizeMode: z.union(["auto", "selected"]).default("auto"),
6061
+ editFormat: z.union([
6062
+ z.const("multipart"),
6063
+ z.const("jsonImageUrlArray"),
6064
+ z.const("formReferenceImages"),
6065
+ z.const("siliconflowJson")
6066
+ ]).default("multipart"),
6067
+ editExtra: z.dict(z.any()).default({}),
6068
+ sizes: z.dict(z.dict(z.string())).default({})
6069
+ })).default([])),
6070
+ openaiCompatActiveChannel: volatile(z.string().default("")),
6071
+ openaiCompatEditModel: volatile(z.string().default("")),
5964
6072
  openaiCompatQuality: volatile(z.string().default("")),
5965
6073
  openaiCompatQualityOptions: volatile(z.array(z.string()).default([
5966
6074
  "auto",
@@ -5973,7 +6081,8 @@ const Config = z.object({
5973
6081
  openaiCompatEditFormat: volatile(z.union([
5974
6082
  z.const("multipart"),
5975
6083
  z.const("jsonImageUrlArray"),
5976
- z.const("formReferenceImages")
6084
+ z.const("formReferenceImages"),
6085
+ z.const("siliconflowJson")
5977
6086
  ]).default("multipart")),
5978
6087
  openaiCompatEditExtra: volatile(z.dict(z.any()).default({})),
5979
6088
  openaiCompatSizes: volatile(z.dict(z.dict(z.string())).default({})),
@@ -6097,17 +6206,40 @@ function resolveProvider(config) {
6097
6206
  };
6098
6207
  }
6099
6208
  }
6209
+ /** Project one named relay channel onto the legacy flat fields used by all provider adapters. */
6210
+ function withOpenAICompatChannel(config, channelId) {
6211
+ const channel = resolveOpenAICompatChannel(config, channelId);
6212
+ if (channel.id === "default") return {
6213
+ ...config,
6214
+ openaiCompatActiveChannel: "default"
6215
+ };
6216
+ return {
6217
+ ...config,
6218
+ openaiCompatLabel: channel.label,
6219
+ openaiCompatBaseURL: channel.baseURL,
6220
+ openaiCompatModel: channel.model,
6221
+ openaiCompatModels: channel.models ?? [],
6222
+ openaiCompatEditModel: channel.editModel ?? "",
6223
+ openaiCompatQuality: channel.quality ?? "",
6224
+ openaiCompatQualityOptions: channel.qualityOptions ?? [],
6225
+ openaiCompatExtraBody: channel.extraBody ?? {},
6226
+ openaiCompatEditSizeMode: channel.editSizeMode ?? "auto",
6227
+ openaiCompatEditFormat: channel.editFormat ?? "multipart",
6228
+ openaiCompatEditExtra: channel.editExtra ?? {},
6229
+ openaiCompatSizes: channel.sizes ?? {},
6230
+ openaiCompatActiveChannel: channel.id
6231
+ };
6232
+ }
6100
6233
  /**
6101
6234
  * Apply a per-call provider and/or model override on top of the saved config.
6102
6235
  * `model` is ignored for ComfyUI, whose per-call equivalent is `workflow`.
6103
6236
  */
6104
- function withProviderOverrides(config, provider, model) {
6237
+ function withProviderOverrides(config, provider, model, mode = "generate") {
6105
6238
  const base = provider === void 0 ? { ...config } : {
6106
6239
  ...config,
6107
6240
  provider
6108
6241
  };
6109
- if (model === void 0) return base;
6110
- const trimmed = model.trim();
6242
+ const trimmed = model?.trim() || (mode === "edit" && base.provider === "openai-compat" ? base.openaiCompatEditModel?.trim() : "") || "";
6111
6243
  if (trimmed.length === 0) return base;
6112
6244
  switch (base.provider ?? "google") {
6113
6245
  case "google": return {
@@ -6120,7 +6252,8 @@ function withProviderOverrides(config, provider, model) {
6120
6252
  };
6121
6253
  case "openai-compat": return {
6122
6254
  ...base,
6123
- openaiCompatModel: trimmed
6255
+ openaiCompatModel: trimmed,
6256
+ ...mode === "edit" ? { openaiCompatEditModel: trimmed } : {}
6124
6257
  };
6125
6258
  case "seedream": return {
6126
6259
  ...base,
@@ -6160,6 +6293,7 @@ function migrateOpenAICompatConfig(config) {
6160
6293
  ...config,
6161
6294
  openaiCompatBaseURL: legacyBase,
6162
6295
  ...compatModel.length > 0 ? {} : { openaiCompatModel: config.openaiModel },
6296
+ ...(config.openaiCompatModels?.length ?? 0) > 0 ? {} : { openaiCompatModels: [compatModel || config.openaiModel?.trim() || ""].filter(Boolean) },
6163
6297
  openaiBaseURL: DEFAULT_OPENAI_BASE_URL,
6164
6298
  ...config.provider === "openai" ? { provider: "openai-compat" } : {}
6165
6299
  };
@@ -6182,6 +6316,31 @@ function selectComfyUIWorkflow(active, requested) {
6182
6316
  return workflow;
6183
6317
  }
6184
6318
  //#endregion
6319
+ //#region lib/types/generation-timeout.js
6320
+ const UPSTREAM_TIMEOUT_HINT = "上游可能已扣费或仍在生成,请到服务商后台确认";
6321
+ var GenerationTimeoutError = class extends Error {
6322
+ constructor(timeoutMs) {
6323
+ super(`生图超时(${String(timeoutMs / 1e3)} 秒未返回),请检查网络或代理后重试;${UPSTREAM_TIMEOUT_HINT}`);
6324
+ this.name = "GenerationTimeoutError";
6325
+ }
6326
+ };
6327
+ async function withGenerationTimeout(timeoutMs, signal, operation) {
6328
+ const watchdog = new AbortController();
6329
+ const combinedSignal = AbortSignal.any([signal, watchdog.signal]);
6330
+ const timeout = setTimeout(() => {
6331
+ watchdog.abort();
6332
+ }, timeoutMs);
6333
+ timeout.unref?.();
6334
+ try {
6335
+ return await operation(combinedSignal);
6336
+ } catch (error) {
6337
+ if (watchdog.signal.aborted && !signal.aborted) throw new GenerationTimeoutError(timeoutMs);
6338
+ throw error;
6339
+ } finally {
6340
+ clearTimeout(timeout);
6341
+ }
6342
+ }
6343
+ //#endregion
6185
6344
  //#region node_modules/.pnpm/@deepseek-ai+dsh-credential_98fa173416f048996704a605a96a2b0e/node_modules/@deepseek-ai/dsh-credentials/lib/index.js
6186
6345
  /**
6187
6346
  * Service Definition for the credential-reference capability seam (`ctx.credentials`). Settings and composition files carry
@@ -6222,8 +6381,8 @@ function isCredentialRefName(value) {
6222
6381
  * never count as configured: the host treats an empty stored value as absent,
6223
6382
  * and a whitespace-only key is never sent to a provider.
6224
6383
  */
6225
- async function resolveApiKey(ctx, provider) {
6226
- const value = (await ctx.credentials.resolve(credentialRef(CLOUD_CREDENTIAL_REFS[provider])))?.value.trim() ?? "";
6384
+ async function resolveApiKey(ctx, provider, keyRef) {
6385
+ const value = (await ctx.credentials.resolve(credentialRef(keyRef ?? CLOUD_CREDENTIAL_REFS[provider])))?.value.trim() ?? "";
6227
6386
  return value.length > 0 ? value : void 0;
6228
6387
  }
6229
6388
  /**
@@ -6232,8 +6391,8 @@ async function resolveApiKey(ctx, provider) {
6232
6391
  * wording; without it the message is the browser-facing Chinese one used by
6233
6392
  * the Studio route.
6234
6393
  */
6235
- async function requireApiKey(ctx, provider, tool) {
6236
- const value = await resolveApiKey(ctx, provider);
6394
+ async function requireApiKey(ctx, provider, tool, keyRef) {
6395
+ const value = await resolveApiKey(ctx, provider, keyRef);
6237
6396
  if (value !== void 0) return value;
6238
6397
  throw new Error(tool === void 0 ? `${PROVIDER_DISPLAY_NAMES[provider]} 尚未配置 API Key,请先到 设置 > 插件 > 图像生成 配置` : `${tool} requires the ${PROVIDER_DISPLAY_NAMES[provider]} API key; configure it in Settings > Plugins > Image generation.`);
6239
6398
  }
@@ -8263,6 +8422,39 @@ async function generateOpenAICompatibleImage(input) {
8263
8422
  }), input.apiKey), input.provider, input);
8264
8423
  }
8265
8424
  async function editOpenAICompatibleImage(input) {
8425
+ if (input.editFormat === "siliconflowJson") {
8426
+ const body = {
8427
+ model: input.model,
8428
+ prompt: input.prompt,
8429
+ ...input.editExtra ?? {},
8430
+ ...input.extraBody ?? {}
8431
+ };
8432
+ const model = typeof body.model === "string" ? body.model.trim() : "";
8433
+ if (!model) throw new Error("硅基流动图生图需要编辑模型");
8434
+ body.model = model;
8435
+ if (model === "Qwen/Qwen-Image") throw new Error("Qwen/Qwen-Image 是文生图模型;请配置图生图模型,例如 Qwen/Qwen-Image-Edit");
8436
+ const maxReferences = model === "Qwen/Qwen-Image-Edit-2509" ? 3 : 1;
8437
+ if (input.sourceImages.length < 1 || input.sourceImages.length > maxReferences) throw new Error(`${model} 图生图支持 1–${String(maxReferences)} 张参考图,当前 ${String(input.sourceImages.length)} 张;请精简参考图或选择支持多图的编辑模型`);
8438
+ if ([
8439
+ "size",
8440
+ "image_size",
8441
+ "quality"
8442
+ ].some((key) => key in body)) throw new Error("此硅基流动图生图格式不发送 size、image_size 或 quality;请从图生图额外字段中移除这些字段");
8443
+ delete body.image;
8444
+ delete body.image2;
8445
+ delete body.image3;
8446
+ for (const [index, source] of input.sourceImages.entries()) body[index === 0 ? "image" : `image${String(index + 1)}`] = toDataUrl$2(source);
8447
+ return parseImageResponse(await guardedFetch("siliconflow image edit request", input.signal, () => fetch(imageEndpoint$1(input.baseURL, "generations"), {
8448
+ method: "POST",
8449
+ redirect: "error",
8450
+ signal: input.signal,
8451
+ headers: {
8452
+ authorization: `Bearer ${input.apiKey}`,
8453
+ "content-type": "application/json"
8454
+ },
8455
+ body: JSON.stringify(body)
8456
+ }), input.apiKey), "siliconflow", input);
8457
+ }
8266
8458
  if (input.editFormat === "qwenJson") return parseImageResponse(await guardedFetch("openai-compat image edit request", input.signal, () => fetch(imageEndpoint$1(input.baseURL, "generations"), {
8267
8459
  method: "POST",
8268
8460
  redirect: "error",
@@ -10158,16 +10350,18 @@ function jsonError$3(res, status, code) {
10158
10350
  //#region lib/types/subscription.js
10159
10351
  /** Translate image tool options into each subscription channel's wire inputs. */
10160
10352
  function subscriptionToolParameters(provider, options) {
10161
- if (options.size !== void 0 && options.aspectRatio !== void 0) throw new Error("size 与 aspect_ratio 请只选一个");
10162
- const ratio = options.aspectRatio;
10353
+ const requestedSize = options.size?.trim() || void 0;
10354
+ const ratio = options.aspectRatio?.trim() || void 0;
10355
+ const imageSize = options.imageSize?.trim() || void 0;
10356
+ if (requestedSize !== void 0 && ratio !== void 0) throw new Error("size 与 aspect_ratio 请只选一个;无关可选字段请省略,不要填写空字符串");
10163
10357
  if (ratio !== void 0 && !SUBSCRIPTION_CAPABILITIES[provider].ratioOptions.some((option) => option.value === ratio)) throw new Error(`${provider} 不支持比例 ${ratio}`);
10164
- const size = options.size ?? (ratio === void 0 ? void 0 : provider === "chatgpt-sub" ? SUBSCRIPTION_CAPABILITIES[provider].sizeFor?.(ratio, "auto") : ratio);
10358
+ const size = requestedSize ?? (ratio === void 0 ? void 0 : provider === "chatgpt-sub" ? SUBSCRIPTION_CAPABILITIES[provider].sizeFor?.(ratio, "auto") : ratio);
10165
10359
  let quality;
10166
- if (options.imageSize !== void 0) {
10167
- if (provider === "grok-sub" && (options.imageSize === "1K" || options.imageSize === "2K")) quality = options.imageSize.toLowerCase();
10168
- else if (provider === "google-sub" && options.imageSize === "4K") quality = "hd";
10169
- else if (provider === "google-sub" && options.imageSize === "1K") quality = void 0;
10170
- else throw new Error(`${provider} 不支持清晰度 ${options.imageSize}`);
10360
+ if (imageSize !== void 0) {
10361
+ if (provider === "grok-sub" && (imageSize === "1K" || imageSize === "2K")) quality = imageSize.toLowerCase();
10362
+ else if (provider === "google-sub" && imageSize === "4K") quality = "hd";
10363
+ else if (provider === "google-sub" && imageSize === "1K") quality = void 0;
10364
+ else throw new Error(`${provider} 不支持清晰度 ${imageSize};${provider === "chatgpt-sub" ? "请省略 image_size,使用 size 或 aspect_ratio 指定尺寸" : `image_size 仅支持 ${provider === "grok-sub" ? "1K、2K" : "1K、4K"}`}`);
10171
10365
  }
10172
10366
  return {
10173
10367
  ...size === void 0 ? {} : { size },
@@ -27765,11 +27959,14 @@ function openAICompatStudioProfile(config, model, configured) {
27765
27959
  }
27766
27960
  /** Return only browser-safe capability data. */
27767
27961
  async function describeStudio(ctx, config, subscriptions) {
27768
- const configuredEntries = await Promise.all(CLOUD_IMAGE_PROVIDERS.map(async (provider) => {
27962
+ const compatChannels = resolveOpenAICompatChannels(config);
27963
+ const configuredEntries = await Promise.all([...CLOUD_IMAGE_PROVIDERS.filter((provider) => provider !== "openai-compat").map(async (provider) => {
27769
27964
  return [provider, await resolveApiKey(ctx, provider) !== void 0];
27770
- }));
27965
+ }), ...compatChannels.map(async (channel) => {
27966
+ return [`openai-compat:${channel.id}`, await resolveApiKey(ctx, "openai-compat", channel.keyRef) !== void 0];
27967
+ })]);
27771
27968
  const configured = Object.fromEntries(configuredEntries);
27772
- const cloudProfiles = CLOUD_IMAGE_PROVIDERS.map((provider) => studioProfile(config, provider, configured[provider]));
27969
+ const cloudProfiles = CLOUD_IMAGE_PROVIDERS.flatMap((provider) => provider === "openai-compat" ? compatChannels.map((channel) => studioProfile(config, provider, configured[`openai-compat:${channel.id}`] === true, channel.id)) : [studioProfile(config, provider, configured[provider] === true)]);
27773
27970
  const subStatuses = await Promise.all(SUBSCRIPTION_PROVIDERS.map(async (provider) => {
27774
27971
  return [provider, subscriptions !== void 0 && (await subscriptions.loginStatus(vendorOf(provider))).state === "logged-in"];
27775
27972
  }));
@@ -27778,9 +27975,12 @@ async function describeStudio(ctx, config, subscriptions) {
27778
27975
  });
27779
27976
  const profiles = [...cloudProfiles, ...subProfiles];
27780
27977
  const preferred = config.provider;
27978
+ const activeProvider = isStudioPreferred(preferred) ? preferred : profiles.find((profile) => profile.configured)?.provider ?? "google";
27979
+ const activeChannel = activeProvider === "openai-compat" ? profiles.find((profile) => profile.provider === activeProvider && profile.channel === resolveOpenAICompatChannel(config).id)?.channel ?? profiles.find((profile) => profile.provider === activeProvider)?.channel : void 0;
27781
27980
  return {
27782
27981
  providers: profiles,
27783
- activeProvider: isStudioPreferred(preferred) ? preferred : profiles.find((profile) => profile.configured)?.provider ?? "google"
27982
+ activeProvider,
27983
+ ...activeChannel === void 0 ? {} : { activeChannel }
27784
27984
  };
27785
27985
  }
27786
27986
  /** The persisted default provider is usable directly when the workbench can drive it. */
@@ -27816,12 +28016,14 @@ async function generateFromStudio(ctx, config, input, signal, fallbackWorkspaceR
27816
28016
  assertAllowed(subscriptionStudioProfile(input.provider, true), input);
27817
28017
  return generateSubscriptionFromStudio(ctx, subscriptions, input, signal);
27818
28018
  }
27819
- assertAllowed(studioProfile(config, input.provider, true), input);
27820
- const active = resolveProvider(withProviderOverrides(config, input.provider, input.model));
28019
+ const channel = input.provider === "openai-compat" ? resolveOpenAICompatChannel(config, input.channel) : void 0;
28020
+ const requestConfig = input.provider === "openai-compat" ? withOpenAICompatChannel(config, input.channel) : config;
28021
+ assertAllowed(studioProfile(requestConfig, input.provider, true, channel?.id), input);
28022
+ const active = resolveProvider(withProviderOverrides(requestConfig, input.provider, input.model));
27821
28023
  if (active.provider === "comfyui") throw new Error("ComfyUI 暂未接入工作台");
27822
28024
  if (isSubscriptionProvider(active.provider)) throw new Error("订阅生图应通过订阅渠道执行");
27823
28025
  const wired = active;
27824
- const credential = await requireApiKey(ctx, input.provider);
28026
+ const credential = await requireApiKey(ctx, input.provider, void 0, channel?.keyRef);
27825
28027
  const rawRefs = input.references ?? (input.reference ? [input.reference] : []);
27826
28028
  if (input.mode === "edit" && rawRefs.length === 0) throw new Error("图生图需要至少一张参考图");
27827
28029
  const sourceImages = input.mode === "edit" ? await Promise.all(rawRefs.map((ref) => readStudioReference(ctx, ref, signal))) : [];
@@ -27858,16 +28060,16 @@ async function generateFromStudio(ctx, config, input, signal, fallbackWorkspaceR
27858
28060
  output = `${aspectRatio}, ${imageSize}`;
27859
28061
  } else if (wired.provider === "openai" || wired.provider === "openai-compat" || wired.provider === "xai" || wired.provider === "zhipu") {
27860
28062
  const p = wired.provider;
27861
- const officialTable = p === "openai" ? officialSizeTable(config, wired.model) : void 0;
28063
+ const officialTable = p === "openai" ? officialSizeTable(requestConfig, wired.model) : void 0;
27862
28064
  const officialResolution = input.resolution ?? (officialTable ? openAICompatTable({ openaiCompatSizes: officialTable }).find((group) => group.ratio === input.ratio)?.tiers[0]?.tier : void 0) ?? "1K";
27863
- const size = p === "openai-compat" ? openAIRequestSize(config, input.ratio, input.resolution ?? input.quality) : p === "openai" ? openAIRequestSize({ openaiCompatSizes: officialTable }, input.ratio, officialResolution) : p === "xai" ? void 0 : CLOUD_CAPABILITIES[p].sizeFor?.(input.ratio, input.quality);
28065
+ const size = p === "openai-compat" ? openAIRequestSize(requestConfig, input.ratio, input.resolution ?? input.quality) : p === "openai" ? openAIRequestSize({ openaiCompatSizes: officialTable }, input.ratio, officialResolution) : p === "xai" ? void 0 : CLOUD_CAPABILITIES[p].sizeFor?.(input.ratio, input.quality);
27864
28066
  if (p === "openai" && size !== void 0) validateOfficialSize(wired.model, size);
27865
28067
  const requestedQuality = input.generationQuality ?? input.quality;
27866
- const quality = p === "openai" ? requestedQuality === "auto" ? void 0 : requestedQuality : p === "openai-compat" ? compatRequestQuality(config, input.generationQuality) : p === "zhipu" ? "hd" : void 0;
28068
+ const quality = p === "openai" ? requestedQuality === "auto" ? void 0 : requestedQuality : p === "openai-compat" ? compatRequestQuality(requestConfig, input.generationQuality) : p === "zhipu" ? "hd" : void 0;
27867
28069
  const extraBody = p === "xai" ? {
27868
28070
  aspect_ratio: input.ratio,
27869
28071
  resolution: input.quality
27870
- } : p === "openai-compat" ? input.mode === "generate" ? compatGenerationExtraBody(config, input.watermark) : compatEditExtraBody(config, input.watermark) : void 0;
28072
+ } : p === "openai-compat" ? input.mode === "generate" ? compatGenerationExtraBody(requestConfig, input.watermark) : compatEditExtraBody(requestConfig, input.watermark) : void 0;
27871
28073
  generated = input.mode === "edit" ? await editOpenAICompatibleImage({
27872
28074
  apiKey: credential,
27873
28075
  baseURL: wired.baseURL,
@@ -27882,7 +28084,7 @@ async function generateFromStudio(ctx, config, input, signal, fallbackWorkspaceR
27882
28084
  ...p === "openai-compat" ? {
27883
28085
  editFormat: wired.editFormat,
27884
28086
  editExtra: wired.editExtra,
27885
- editSizeMode: config.openaiCompatEditSizeMode ?? "auto"
28087
+ editSizeMode: requestConfig.openaiCompatEditSizeMode ?? "auto"
27886
28088
  } : p === "xai" ? { editFormat: "xaiJson" } : {}
27887
28089
  }) : await generateOpenAICompatibleImage({
27888
28090
  provider: p,
@@ -27967,6 +28169,7 @@ async function generateFromStudio(ctx, config, input, signal, fallbackWorkspaceR
27967
28169
  attachment: single.attachment,
27968
28170
  output: single.output,
27969
28171
  provider: input.provider,
28172
+ ...channel === void 0 ? {} : { channel: channel.id },
27970
28173
  model: input.model,
27971
28174
  prompt: input.prompt,
27972
28175
  createdAt: Date.now(),
@@ -27997,6 +28200,7 @@ async function generateFromStudio(ctx, config, input, signal, fallbackWorkspaceR
27997
28200
  attachment: first.attachment,
27998
28201
  output: first.output,
27999
28202
  provider: input.provider,
28203
+ ...channel === void 0 ? {} : { channel: channel.id },
28000
28204
  model: input.model,
28001
28205
  prompt: input.prompt,
28002
28206
  createdAt: Date.now(),
@@ -28120,11 +28324,19 @@ function subscriptionResponse(input, startedAt, requestedCount, failedCount, ite
28120
28324
  ...errors !== void 0 && errors.length > 0 ? { errors } : {}
28121
28325
  };
28122
28326
  }
28123
- function studioProfile(config, provider, configured) {
28124
- const result = cloudStudioProfile(config, provider, configured);
28327
+ function studioProfile(config, provider, configured, channelId) {
28328
+ const channel = provider === "openai-compat" && channelId !== void 0 ? resolveOpenAICompatChannel(config, channelId) : void 0;
28329
+ const scoped = channel === void 0 ? config : withOpenAICompatChannel(config, channel.id);
28330
+ const result = cloudStudioProfile(scoped, provider, configured);
28331
+ const editModel = provider === "openai-compat" ? scoped.openaiCompatEditModel?.trim() : void 0;
28125
28332
  return {
28126
28333
  ...result,
28127
- ...watermarkProfile(config, provider, result.model)
28334
+ ...channel === void 0 ? {} : {
28335
+ channel: channel.id,
28336
+ label: channel.label
28337
+ },
28338
+ ...editModel ? { editModel } : {},
28339
+ ...watermarkProfile(scoped, provider, result.model)
28128
28340
  };
28129
28341
  }
28130
28342
  function cloudStudioProfile(config, provider, configured) {
@@ -28163,7 +28375,14 @@ function cloudStudioProfile(config, provider, configured) {
28163
28375
  /** Output capabilities remain visible before credentials, address or model are configured. */
28164
28376
  function compatibleStudioProfile(config, model, configured) {
28165
28377
  const groups = openAICompatTable(config);
28166
- const qualityProfile = {
28378
+ const capabilityProfile = {
28379
+ modelOptions: resolveOpenAICompatModels({
28380
+ ...config,
28381
+ openaiCompatModel: model
28382
+ }).map((value) => ({
28383
+ value,
28384
+ label: value
28385
+ })),
28167
28386
  generationQualityOptions: compatQualityOptions(config),
28168
28387
  defaultGenerationQuality: compatRequestQuality(config) ?? "__default__",
28169
28388
  resolutionOptions: Object.fromEntries(groups.map((group) => [group.ratio, group.tiers.map((entry) => ({
@@ -28180,7 +28399,7 @@ function compatibleStudioProfile(config, model, configured) {
28180
28399
  value: "standard",
28181
28400
  label: "标准(推荐)"
28182
28401
  }], "1:1", "standard"),
28183
- ...qualityProfile,
28402
+ ...capabilityProfile,
28184
28403
  resolutionOptions: Object.fromEntries(groups.map((group) => [group.ratio, [{
28185
28404
  value: "standard",
28186
28405
  label: `标准 · ${group.tiers[0].size}`
@@ -28188,7 +28407,7 @@ function compatibleStudioProfile(config, model, configured) {
28188
28407
  };
28189
28408
  return {
28190
28409
  ...openAICompatStudioProfile(config, model, configured),
28191
- ...qualityProfile
28410
+ ...capabilityProfile
28192
28411
  };
28193
28412
  }
28194
28413
  function profile(provider, model, configured, ratioOptions, qualityOptions, defaultRatio, defaultQuality) {
@@ -28221,7 +28440,7 @@ function option(value) {
28221
28440
  };
28222
28441
  }
28223
28442
  function assertAllowed(profile, input) {
28224
- if (input.model !== profile.model) throw new Error("模型配置已变化,请刷新工作台后重试");
28443
+ if (!studioModelOptionsForMode(profile, input.mode).some((option) => option.value === input.model)) throw new Error("模型配置已变化,请刷新工作台后重试");
28225
28444
  if (!profile.ratioOptions.some((option) => option.value === input.ratio)) throw new Error("该 Provider 不支持所选比例");
28226
28445
  const selectedQuality = input.provider === "openai" ? input.generationQuality ?? input.quality : input.resolution ?? input.quality;
28227
28446
  if (!profile.qualityOptions.some((option) => option.value === selectedQuality)) throw new Error("该 Provider 不支持所选清晰度");
@@ -28272,8 +28491,6 @@ async function serveStudio(req, res, deps) {
28272
28491
  if (!(req.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) return jsonError$1(res, 415, "json-required");
28273
28492
  const controller = new AbortController();
28274
28493
  const watchdog = new AbortController();
28275
- const CLOUD_WATCHDOG_MS = 15e4;
28276
- let subscriptionChannel = false;
28277
28494
  const signal = AbortSignal.any([controller.signal, watchdog.signal]);
28278
28495
  const onConnectionClose = () => {
28279
28496
  if (!res.writableEnded) controller.abort(/* @__PURE__ */ new Error("The browser closed the image generation request."));
@@ -28290,10 +28507,12 @@ async function serveStudio(req, res, deps) {
28290
28507
  } catch (error) {
28291
28508
  return jsonError$1(res, 400, errorMessage(error, "invalid-request"));
28292
28509
  }
28293
- subscriptionChannel = isSubscriptionProvider(input.provider);
28294
- setTimeout(() => {
28510
+ const subscriptionChannel = isSubscriptionProvider(input.provider);
28511
+ const timeoutMs = subscriptionChannel ? SUBSCRIPTION_TIMEOUT_MS : deps.timeoutMs ?? 15e4;
28512
+ const timeout = setTimeout(() => {
28295
28513
  watchdog.abort();
28296
- }, subscriptionChannel ? SUBSCRIPTION_TIMEOUT_MS : CLOUD_WATCHDOG_MS).unref?.();
28514
+ }, timeoutMs);
28515
+ timeout.unref?.();
28297
28516
  res.writeHead(200, {
28298
28517
  "content-type": "application/json; charset=utf-8",
28299
28518
  "cache-control": "no-store",
@@ -28308,11 +28527,12 @@ async function serveStudio(req, res, deps) {
28308
28527
  res.end(JSON.stringify(output));
28309
28528
  } catch (error) {
28310
28529
  if (res.destroyed || res.writableEnded) return;
28311
- const message = watchdog.signal.aborted ? subscriptionChannel ? "上游生成超时(订阅通道 300 秒未返回),请稍后重试" : "上游生成超时(150 秒未返回),请检查网络或代理后重试" : errorMessage(error, "generation-failed");
28530
+ const message = watchdog.signal.aborted ? subscriptionChannel ? `上游生成超时(订阅通道 ${String(Math.round(timeoutMs / 1e3))} 秒未返回),请稍后重试;${UPSTREAM_TIMEOUT_HINT}` : `上游生成超时(${String(Math.round(timeoutMs / 1e3))} 秒未返回),请检查网络或代理后重试;${UPSTREAM_TIMEOUT_HINT}` : errorMessage(error, "generation-failed");
28312
28531
  res.end(JSON.stringify({ error: message }));
28313
28532
  }
28314
28533
  } finally {
28315
28534
  clearInterval(heartbeat);
28535
+ clearTimeout(timeout);
28316
28536
  }
28317
28537
  } catch (error) {
28318
28538
  if (res.headersSent || res.writableEnded || res.destroyed) return;
@@ -28328,6 +28548,8 @@ function parseStudioGenerateRequest(value) {
28328
28548
  if (input === void 0) throw new Error("请求格式无效");
28329
28549
  if (input.mode !== "generate" && input.mode !== "edit") throw new Error("请选择生成类型");
28330
28550
  if (!studioProvider(input.provider)) throw new Error("不支持该图像 Provider");
28551
+ if (input.channel !== void 0 && input.provider !== "openai-compat") throw new Error("仅 OpenAI 兼容 Provider 支持选择渠道");
28552
+ const channel = input.channel === void 0 ? void 0 : requiredText(input.channel, "请选择渠道", 100);
28331
28553
  const prompt = requiredText(input.prompt, "请输入提示词");
28332
28554
  const model = requiredText(input.model, "请选择模型", 200);
28333
28555
  const ratio = requiredText(input.ratio, "请选择比例", 32);
@@ -28348,6 +28570,7 @@ function parseStudioGenerateRequest(value) {
28348
28570
  return {
28349
28571
  mode: input.mode,
28350
28572
  provider: input.provider,
28573
+ ...channel === void 0 ? {} : { channel },
28351
28574
  model,
28352
28575
  prompt,
28353
28576
  ratio,
@@ -28706,12 +28929,17 @@ async function serveTestConnection(req, res, deps) {
28706
28929
  const provider = record(body)?.provider;
28707
28930
  if (typeof provider !== "string" || !IMAGE_PROVIDERS.includes(provider)) return jsonError(res, 400, "invalid-provider");
28708
28931
  const active = provider;
28709
- if (record(body)?.action === "models") {
28932
+ const action = record(body)?.action;
28933
+ const rawChannel = record(body)?.channel;
28934
+ if (rawChannel !== void 0 && (typeof rawChannel !== "string" || rawChannel.trim().length === 0 || active !== "openai-compat")) return jsonError(res, 400, "invalid-channel");
28935
+ const channel = typeof rawChannel === "string" ? rawChannel.trim() : void 0;
28936
+ const requestConfig = () => active === "openai-compat" ? withOpenAICompatChannel(deps.config(), channel) : deps.config();
28937
+ if (action === "models") {
28710
28938
  if (active === "comfyui" || isSubscriptionProvider(active)) return jsonError(res, 400, "models-unsupported");
28711
28939
  let result;
28712
28940
  try {
28713
- const apiKey = await deps.resolveKey(active);
28714
- result = active === "google" ? await fetchGoogleImageModels(deps.config(), apiKey) : active === "dashscope" ? await fetchDashScopeImageModels(deps.config(), apiKey) : await fetchOpenAIImageModels(active, deps.config(), apiKey);
28941
+ const apiKey = await deps.resolveKey(active, channel);
28942
+ result = active === "google" ? await fetchGoogleImageModels(requestConfig(), apiKey) : active === "dashscope" ? await fetchDashScopeImageModels(requestConfig(), apiKey) : await fetchOpenAIImageModels(active, requestConfig(), apiKey);
28715
28943
  } catch (error) {
28716
28944
  result = {
28717
28945
  ok: false,
@@ -28726,8 +28954,8 @@ async function serveTestConnection(req, res, deps) {
28726
28954
  if (active === "comfyui") result = await probeComfyUIConnection(deps.config());
28727
28955
  else if (isSubscriptionProvider(active)) result = await probeSubscriptionConnection(active, deps.subscriptionManager);
28728
28956
  else {
28729
- const apiKey = await deps.resolveKey(active);
28730
- result = await probeProviderConnection(active, deps.config(), apiKey);
28957
+ const apiKey = await deps.resolveKey(active, channel);
28958
+ result = await probeProviderConnection(active, requestConfig(), apiKey);
28731
28959
  }
28732
28960
  } catch (error) {
28733
28961
  result = {
@@ -28791,20 +29019,65 @@ function serveSettingsHealth(req, res, health) {
28791
29019
  res.end(JSON.stringify(health));
28792
29020
  }
28793
29021
  //#endregion
29022
+ //#region lib/types/image-tool-context.js
29023
+ /** Live, credential-free instructions for the conversation model's image tools. */
29024
+ function imageToolContext(config) {
29025
+ const provider = config.provider ?? "google";
29026
+ let model = "not configured";
29027
+ let editModel = model;
29028
+ try {
29029
+ const active = resolveProvider(config);
29030
+ model = active.provider === "comfyui" ? active.workflow?.name ?? "workflow selected in settings" : active.model;
29031
+ const edit = resolveProvider(withProviderOverrides(config, void 0, void 0, "edit"));
29032
+ editModel = edit.provider === "comfyui" ? edit.workflow?.name ?? model : edit.model;
29033
+ } catch {}
29034
+ return [
29035
+ `Image tools current default provider: ${provider}; generation model: ${JSON.stringify(model)}; editing model: ${JSON.stringify(editModel)}.`,
29036
+ "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.",
29037
+ "Omit unused optional arguments entirely; never fill them with empty strings. size and aspect_ratio are alternatives: provide at most one.",
29038
+ "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.",
29039
+ "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."
29040
+ ].join("\n");
29041
+ }
29042
+ //#endregion
28794
29043
  //#region lib/types/index.js
28795
29044
  const name = "dsh-image-gen";
28796
29045
  const inject = [
28797
29046
  "tools",
28798
29047
  "attachments",
28799
- "credentials",
28800
- "webServer"
29048
+ "credentials"
28801
29049
  ];
29050
+ /** Optional strings from tool callers may contain empty placeholders (#86). */
29051
+ function cleanImageToolArgs(args) {
29052
+ const clean = { ...args };
29053
+ const fields = clean;
29054
+ for (const key of [
29055
+ "model",
29056
+ "channel",
29057
+ "size",
29058
+ "aspect_ratio",
29059
+ "image_size",
29060
+ "quality",
29061
+ "workflow"
29062
+ ]) {
29063
+ const value = fields[key];
29064
+ if (typeof value !== "string") continue;
29065
+ if (value.trim().length === 0) delete fields[key];
29066
+ else fields[key] = value.trim();
29067
+ }
29068
+ return clean;
29069
+ }
28802
29070
  /** Validate the untrusted per-call provider override from tool arguments. */
28803
29071
  function providerOverrideOf(value) {
28804
29072
  if (value === void 0 || value === null || value === "") return void 0;
28805
29073
  if (typeof value !== "string" || !IMAGE_PROVIDERS.includes(value)) throw new Error(`Unsupported provider ${JSON.stringify(value)}. Supported providers: ${IMAGE_PROVIDERS.join(", ")}.`);
28806
29074
  return value;
28807
29075
  }
29076
+ function configForToolCall(config, provider, channel) {
29077
+ const selected = provider ?? config.provider ?? "google";
29078
+ if (channel !== void 0 && selected !== "openai-compat") throw new Error("The channel argument is only supported with provider=openai-compat.");
29079
+ return selected === "openai-compat" ? withOpenAICompatChannel(config, channel) : config;
29080
+ }
28808
29081
  function apply(ctx, config = {}) {
28809
29082
  const plainConfig = (source) => {
28810
29083
  const out = {};
@@ -28815,143 +29088,155 @@ function apply(ctx, config = {}) {
28815
29088
  const knownWorkspaceRoots = /* @__PURE__ */ new Set();
28816
29089
  const canvasMirror = new CanvasMirror();
28817
29090
  const subscriptionManager = new SubscriptionManager(ctx);
28818
- registerSubscriptionRoutes(ctx, subscriptionManager);
28819
29091
  const settingsHealth = installImageSettings(ctx, current(), {
28820
29092
  setSource: (source) => {
28821
29093
  current = () => migrateOpenAICompatConfig(plainConfig(source()));
28822
29094
  },
28823
29095
  onChange: () => {}
28824
29096
  });
28825
- ctx.effect(() => ctx.webServer.register({
28826
- kind: "exact",
28827
- path: SETTINGS_HEALTH_ROUTE,
28828
- handler: (req, res) => serveSettingsHealth(req, res, settingsHealth())
28829
- }), "dsh-image-gen: settings health");
28830
- ctx.effect(() => ctx.webServer.register({
28831
- kind: "exact",
28832
- path: IMAGE_ROUTE,
28833
- handler: (req, res) => serveImage(req, res, { readImage: (ref) => ctx.attachments.readImage(ref) })
28834
- }), "dsh-image-gen: image route");
28835
- ctx.effect(() => ctx.webServer.register({
28836
- kind: "exact",
28837
- path: IMPORT_ROUTE,
28838
- handler: (req, res) => serveImport(req, res, {
28839
- saveImage: (image) => ctx.attachments.saveImage(image),
28840
- maxImageBytes: ctx.attachments.imageLimits.maxImageBytes,
28841
- mediaTypes: ctx.attachments.imageLimits.mediaTypes
28842
- })
28843
- }), "dsh-image-gen: import route");
28844
- ctx.effect(() => ctx.webServer.register({
28845
- kind: "exact",
28846
- path: DELETE_ROUTE,
28847
- handler: (req, res) => serveDelete(req, res, { deleteWorkspaceImage: async (filePath) => {
28848
- const discovered = await getDshWorkspaceRoots().catch(() => []);
28849
- return deleteImageFromWorkspace(filePath, /* @__PURE__ */ new Set([
28850
- ...knownWorkspaceRoots,
28851
- ...discovered,
28852
- process.cwd()
28853
- ]));
28854
- } })
28855
- }), "dsh-image-gen: delete route");
28856
- ctx.effect(() => ctx.webServer.register({
28857
- kind: "exact",
28858
- path: SAVE_WORKSPACE_ROUTE,
28859
- handler: (req, res) => serveSaveWorkspace(req, res, {
28860
- readImage: (ref) => ctx.attachments.readImage(ref),
28861
- saveToWorkspace: (options) => {
28862
- if (options.workspaceRoot) knownWorkspaceRoots.add(options.workspaceRoot);
28863
- return saveImageToWorkspace({
28864
- workspaceRoot: options.workspaceRoot,
28865
- folder: current().workspaceFolder,
28866
- attachmentId: options.attachmentId,
28867
- mediaType: options.mediaType,
28868
- data: options.data
28869
- });
28870
- },
28871
- getActiveWorkspaceRoot: () => Array.from(knownWorkspaceRoots)[0] || process.cwd(),
28872
- getAllowedWorkspaceRoots: async () => {
29097
+ ctx.inject(["webServer"], (webCtx) => {
29098
+ registerSubscriptionRoutes(webCtx, subscriptionManager);
29099
+ webCtx.effect(() => webCtx.webServer.register({
29100
+ kind: "exact",
29101
+ path: SETTINGS_HEALTH_ROUTE,
29102
+ handler: (req, res) => serveSettingsHealth(req, res, settingsHealth())
29103
+ }), "dsh-image-gen: settings health");
29104
+ webCtx.effect(() => webCtx.webServer.register({
29105
+ kind: "exact",
29106
+ path: IMAGE_ROUTE,
29107
+ handler: (req, res) => serveImage(req, res, { readImage: (ref) => ctx.attachments.readImage(ref) })
29108
+ }), "dsh-image-gen: image route");
29109
+ webCtx.effect(() => webCtx.webServer.register({
29110
+ kind: "exact",
29111
+ path: IMPORT_ROUTE,
29112
+ handler: (req, res) => serveImport(req, res, {
29113
+ saveImage: (image) => ctx.attachments.saveImage(image),
29114
+ maxImageBytes: ctx.attachments.imageLimits.maxImageBytes,
29115
+ mediaTypes: ctx.attachments.imageLimits.mediaTypes
29116
+ })
29117
+ }), "dsh-image-gen: import route");
29118
+ webCtx.effect(() => webCtx.webServer.register({
29119
+ kind: "exact",
29120
+ path: DELETE_ROUTE,
29121
+ handler: (req, res) => serveDelete(req, res, { deleteWorkspaceImage: async (filePath) => {
28873
29122
  const discovered = await getDshWorkspaceRoots().catch(() => []);
28874
- return /* @__PURE__ */ new Set([
29123
+ return deleteImageFromWorkspace(filePath, /* @__PURE__ */ new Set([
28875
29124
  ...knownWorkspaceRoots,
28876
29125
  ...discovered,
28877
29126
  process.cwd()
28878
- ]);
28879
- },
28880
- isSaveEnabled: () => current().saveToWorkspace !== false
28881
- })
28882
- }), "dsh-image-gen: save workspace route");
28883
- ctx.effect(() => ctx.webServer.register({
28884
- kind: "exact",
28885
- path: TEST_CONNECTION_ROUTE,
28886
- handler: (req, res) => serveTestConnection(req, res, {
28887
- resolveKey: (provider) => resolveApiKey(ctx, provider),
28888
- config: () => current(),
28889
- subscriptionManager
28890
- })
28891
- }), "dsh-image-gen: test connection route");
28892
- ctx.effect(() => ctx.webServer.register({
28893
- kind: "exact",
28894
- path: CANVAS_STATE_ROUTE,
28895
- handler: (req, res) => serveCanvasState(req, res, {
28896
- mirror: canvasMirror,
28897
- maxBodyBytes: Math.ceil(ctx.attachments.imageLimits.maxImageBytes * 1.4) + 262144,
28898
- maxImageBytes: ctx.attachments.imageLimits.maxImageBytes
28899
- })
28900
- }), "dsh-image-gen: canvas state route");
28901
- ctx.effect(() => ctx.webServer.register({
28902
- kind: "exact",
28903
- path: CANVAS_ASSET_ROUTE,
28904
- handler: (req, res) => serveCanvasAsset(req, res, {
28905
- maxImageBytes: ctx.attachments.imageLimits.maxImageBytes,
28906
- saveImage: (image) => ctx.attachments.saveImage(image)
28907
- })
28908
- }), "dsh-image-gen: canvas asset route");
29127
+ ]));
29128
+ } })
29129
+ }), "dsh-image-gen: delete route");
29130
+ webCtx.effect(() => webCtx.webServer.register({
29131
+ kind: "exact",
29132
+ path: SAVE_WORKSPACE_ROUTE,
29133
+ handler: (req, res) => serveSaveWorkspace(req, res, {
29134
+ readImage: (ref) => ctx.attachments.readImage(ref),
29135
+ saveToWorkspace: (options) => {
29136
+ if (options.workspaceRoot) knownWorkspaceRoots.add(options.workspaceRoot);
29137
+ return saveImageToWorkspace({
29138
+ workspaceRoot: options.workspaceRoot,
29139
+ folder: current().workspaceFolder,
29140
+ attachmentId: options.attachmentId,
29141
+ mediaType: options.mediaType,
29142
+ data: options.data
29143
+ });
29144
+ },
29145
+ getActiveWorkspaceRoot: () => Array.from(knownWorkspaceRoots)[0] || process.cwd(),
29146
+ getAllowedWorkspaceRoots: async () => {
29147
+ const discovered = await getDshWorkspaceRoots().catch(() => []);
29148
+ return /* @__PURE__ */ new Set([
29149
+ ...knownWorkspaceRoots,
29150
+ ...discovered,
29151
+ process.cwd()
29152
+ ]);
29153
+ },
29154
+ isSaveEnabled: () => current().saveToWorkspace !== false
29155
+ })
29156
+ }), "dsh-image-gen: save workspace route");
29157
+ webCtx.effect(() => webCtx.webServer.register({
29158
+ kind: "exact",
29159
+ path: TEST_CONNECTION_ROUTE,
29160
+ handler: (req, res) => serveTestConnection(req, res, {
29161
+ resolveKey: (provider, channel) => resolveApiKey(ctx, provider, provider === "openai-compat" ? resolveOpenAICompatChannel(current(), channel).keyRef : void 0),
29162
+ config: () => current(),
29163
+ subscriptionManager
29164
+ })
29165
+ }), "dsh-image-gen: test connection route");
29166
+ webCtx.effect(() => webCtx.webServer.register({
29167
+ kind: "exact",
29168
+ path: CANVAS_STATE_ROUTE,
29169
+ handler: (req, res) => serveCanvasState(req, res, {
29170
+ mirror: canvasMirror,
29171
+ maxBodyBytes: Math.ceil(ctx.attachments.imageLimits.maxImageBytes * 1.4) + 262144,
29172
+ maxImageBytes: ctx.attachments.imageLimits.maxImageBytes
29173
+ })
29174
+ }), "dsh-image-gen: canvas state route");
29175
+ webCtx.effect(() => webCtx.webServer.register({
29176
+ kind: "exact",
29177
+ path: CANVAS_ASSET_ROUTE,
29178
+ handler: (req, res) => serveCanvasAsset(req, res, {
29179
+ maxImageBytes: ctx.attachments.imageLimits.maxImageBytes,
29180
+ saveImage: (image) => ctx.attachments.saveImage(image)
29181
+ })
29182
+ }), "dsh-image-gen: canvas asset route");
29183
+ });
28909
29184
  ctx.inject(["systemPrompt"], (promptCtx) => {
28910
- promptCtx.systemPrompt.context({
29185
+ promptCtx.effect(() => promptCtx.systemPrompt.context({
29186
+ name: "dsh-image-gen:image-defaults",
29187
+ order: 59,
29188
+ text: () => imageToolContext(current())
29189
+ }), "dsh-image-gen: image tool context");
29190
+ promptCtx.effect(() => promptCtx.systemPrompt.context({
28911
29191
  name: "dsh-image-gen:canvas",
28912
29192
  order: 60,
28913
29193
  text: () => canvasMirror.digest()
28914
- });
29194
+ }), "dsh-image-gen: canvas context");
28915
29195
  });
28916
29196
  registerCanvasTools(ctx, canvasMirror, { persistSelectionImage: (image) => ctx.attachments.saveImage({
28917
29197
  data: image.data,
28918
29198
  mediaType: image.mediaType,
28919
29199
  name: "canvas-selection"
28920
29200
  }) });
28921
- ctx.effect(() => ctx.webServer.register({
28922
- kind: "exact",
28923
- path: STUDIO_ROUTE,
28924
- handler: (req, res) => serveStudio(req, res, {
28925
- describe: async () => {
28926
- const base = await describeStudio(ctx, current(), subscriptionManager);
28927
- const workspaces = await getDshWorkspacesFull().catch(() => []);
28928
- const activeRoot = Array.from(knownWorkspaceRoots)[0] || workspaces[0]?.path || process.cwd();
28929
- return {
28930
- ...base,
28931
- workspaceRoot: activeRoot,
28932
- workspaces
28933
- };
28934
- },
28935
- generate: (input, signal) => {
28936
- const fallbackRoot = Array.from(knownWorkspaceRoots)[0] || process.cwd();
28937
- return generateFromStudio(ctx, current(), input, signal, fallbackRoot, subscriptionManager);
28938
- },
28939
- maxBodyBytes: Math.ceil(ctx.attachments.imageLimits.maxImageBytes * 1.4 * 5) + 262144
28940
- })
28941
- }), "dsh-image-gen: studio route");
28942
- const serveInspiration = createInspirationRoute();
28943
- ctx.effect(() => ctx.webServer.register({
28944
- kind: "prefix",
28945
- path: INSPIRATION_ROUTE,
28946
- handler: (req, res) => {
28947
- const originalUrl = req.url ?? "/";
28948
- req.url = originalUrl.startsWith("/plugins/dsh-image-gen/inspiration") ? originalUrl.slice(34) || "/" : originalUrl;
28949
- return serveInspiration(req, res);
28950
- }
28951
- }), "dsh-image-gen: inspiration route");
29201
+ ctx.inject(["webServer"], (webCtx) => {
29202
+ webCtx.effect(() => webCtx.webServer.register({
29203
+ kind: "exact",
29204
+ path: STUDIO_ROUTE,
29205
+ handler: (req, res) => serveStudio(req, res, {
29206
+ describe: async () => {
29207
+ const base = await describeStudio(ctx, current(), subscriptionManager);
29208
+ const workspaces = await getDshWorkspacesFull().catch(() => []);
29209
+ const activeRoot = Array.from(knownWorkspaceRoots)[0] || workspaces[0]?.path || process.cwd();
29210
+ return {
29211
+ ...base,
29212
+ workspaceRoot: activeRoot,
29213
+ workspaces
29214
+ };
29215
+ },
29216
+ generate: (input, signal) => {
29217
+ const fallbackRoot = Array.from(knownWorkspaceRoots)[0] || process.cwd();
29218
+ return generateFromStudio(ctx, current(), input, signal, fallbackRoot, subscriptionManager);
29219
+ },
29220
+ maxBodyBytes: Math.ceil(ctx.attachments.imageLimits.maxImageBytes * 1.4 * 5) + 262144,
29221
+ timeoutMs: current().cloudTimeoutMs ?? 15e4
29222
+ })
29223
+ }), "dsh-image-gen: studio route");
29224
+ const serveInspiration = createInspirationRoute();
29225
+ webCtx.effect(() => webCtx.webServer.register({
29226
+ kind: "prefix",
29227
+ path: INSPIRATION_ROUTE,
29228
+ handler: (req, res) => {
29229
+ const originalUrl = req.url ?? "/";
29230
+ req.url = originalUrl.startsWith("/plugins/dsh-image-gen/inspiration") ? originalUrl.slice(34) || "/" : originalUrl;
29231
+ return serveInspiration(req, res);
29232
+ }
29233
+ }), "dsh-image-gen: inspiration route");
29234
+ });
28952
29235
  /** Per-item generation request shared by generate_image and generate_images. */
28953
- const generateSingle = async (args, exec) => {
28954
- const active = resolveProvider(withProviderOverrides(current(), providerOverrideOf(args.provider), args.model));
29236
+ const generateSingle = async (rawArgs, exec) => {
29237
+ const args = cleanImageToolArgs(rawArgs);
29238
+ const requestConfig = configForToolCall(current(), providerOverrideOf(args.provider), args.channel);
29239
+ const active = resolveProvider(withProviderOverrides(requestConfig, providerOverrideOf(args.provider), args.model));
28955
29240
  if (active.provider === "comfyui") {
28956
29241
  const workflow = selectComfyUIWorkflow(active, args.workflow);
28957
29242
  return saveGenerated(ctx, await generateComfyUIImage({
@@ -28978,11 +29263,12 @@ function apply(ctx, config = {}) {
28978
29263
  signal: exec.signal
28979
29264
  }), active.provider, active.model, "subscription", current(), exec, knownWorkspaceRoots);
28980
29265
  }
28981
- const credential = await requireApiKey(ctx, active.provider, "generate_image");
29266
+ const runCloud = (operation) => withGenerationTimeout(current().cloudTimeoutMs ?? 15e4, exec.signal, operation);
29267
+ const credential = await requireApiKey(ctx, active.provider, "generate_image", active.provider === "openai-compat" ? resolveOpenAICompatChannel(requestConfig, args.channel).keyRef : void 0);
28982
29268
  if (active.provider === "google") {
28983
29269
  const aspectRatio = args.aspect_ratio ?? active.aspectRatio;
28984
29270
  const imageSize = args.image_size ?? active.imageSize;
28985
- return saveGenerated(ctx, await generateGoogleImage({
29271
+ return saveGenerated(ctx, await runCloud((signal) => generateGoogleImage({
28986
29272
  apiKey: credential,
28987
29273
  endpoint: active.endpoint,
28988
29274
  native: active.native,
@@ -28991,20 +29277,20 @@ function apply(ctx, config = {}) {
28991
29277
  aspectRatio,
28992
29278
  imageSize,
28993
29279
  maxBytes: ctx.attachments.imageLimits.maxImageBytes,
28994
- signal: exec.signal
28995
- }), active.provider, active.model, `${aspectRatio}, ${imageSize}`, current(), exec, knownWorkspaceRoots);
29280
+ signal
29281
+ })), active.provider, active.model, `${aspectRatio}, ${imageSize}`, current(), exec, knownWorkspaceRoots);
28996
29282
  }
28997
29283
  if (active.provider === "dashscope") {
28998
29284
  const size = args.size ?? active.imageSize;
28999
- return saveGenerated(ctx, await generateDashScopeImage({
29285
+ return saveGenerated(ctx, await runCloud((signal) => generateDashScopeImage({
29000
29286
  apiKey: credential,
29001
29287
  endpoint: active.endpoint,
29002
29288
  model: active.model,
29003
29289
  prompt: args.prompt,
29004
29290
  size,
29005
29291
  maxBytes: ctx.attachments.imageLimits.maxImageBytes,
29006
- signal: exec.signal
29007
- }), active.provider, active.model, size, current(), exec, knownWorkspaceRoots);
29292
+ signal
29293
+ })), active.provider, active.model, size, current(), exec, knownWorkspaceRoots);
29008
29294
  }
29009
29295
  if (active.provider === "xai") {
29010
29296
  const extraBody = xaiToolParameters({
@@ -29012,7 +29298,7 @@ function apply(ctx, config = {}) {
29012
29298
  ...args.image_size !== void 0 ? { imageSize: args.image_size } : {},
29013
29299
  ...args.size !== void 0 ? { size: args.size } : {}
29014
29300
  });
29015
- return saveGenerated(ctx, await generateOpenAICompatibleImage({
29301
+ return saveGenerated(ctx, await runCloud((signal) => generateOpenAICompatibleImage({
29016
29302
  provider: "xai",
29017
29303
  apiKey: credential,
29018
29304
  baseURL: active.baseURL,
@@ -29020,15 +29306,15 @@ function apply(ctx, config = {}) {
29020
29306
  prompt: args.prompt,
29021
29307
  extraBody,
29022
29308
  maxBytes: ctx.attachments.imageLimits.maxImageBytes,
29023
- signal: exec.signal
29024
- }), active.provider, active.model, extraBody.aspect_ratio ?? "auto", current(), exec, knownWorkspaceRoots);
29309
+ signal
29310
+ })), active.provider, active.model, extraBody.aspect_ratio ?? "auto", current(), exec, knownWorkspaceRoots);
29025
29311
  }
29026
29312
  const size = args.size ?? active.imageSize;
29027
29313
  const arkOptions = active.provider === "seedream" ? active.arkOptions : void 0;
29028
29314
  if (active.provider === "openai") validateOfficialSize(active.model, size);
29029
- const quality = active.provider === "openai-compat" ? compatRequestQuality(current(), args.quality) : active.provider === "openai" ? args.quality : void 0;
29315
+ const quality = active.provider === "openai-compat" ? compatRequestQuality(requestConfig, args.quality) : active.provider === "openai" ? args.quality : void 0;
29030
29316
  if (active.provider === "openai" && quality !== void 0 && !officialQualityOptions(active.model).some((option) => option.value === quality)) throw new Error("该 OpenAI 模型不支持所选生成质量");
29031
- return saveGenerated(ctx, await generateOpenAICompatibleImage({
29317
+ const value = await saveGenerated(ctx, await runCloud((signal) => generateOpenAICompatibleImage({
29032
29318
  provider: active.provider,
29033
29319
  apiKey: credential,
29034
29320
  baseURL: active.baseURL,
@@ -29036,11 +29322,15 @@ function apply(ctx, config = {}) {
29036
29322
  prompt: args.prompt,
29037
29323
  size,
29038
29324
  maxBytes: ctx.attachments.imageLimits.maxImageBytes,
29039
- signal: exec.signal,
29325
+ signal,
29040
29326
  ...quality === void 0 ? {} : { quality },
29041
- ...active.provider === "openai-compat" ? { extraBody: current().openaiCompatExtraBody ?? {} } : {},
29327
+ ...active.provider === "openai-compat" ? { extraBody: requestConfig.openaiCompatExtraBody ?? {} } : {},
29042
29328
  ...arkOptions === void 0 ? {} : { arkOptions }
29043
- }), active.provider, active.model, quality === void 0 ? size : `${size}, quality=${quality}`, current(), exec, knownWorkspaceRoots);
29329
+ })), active.provider, active.model, quality === void 0 ? size : `${size}, quality=${quality}`, current(), exec, knownWorkspaceRoots);
29330
+ return active.provider === "openai-compat" ? {
29331
+ ...value,
29332
+ channel: resolveOpenAICompatChannel(requestConfig, args.channel).id
29333
+ } : value;
29044
29334
  };
29045
29335
  ctx.tools.register(defineTool({
29046
29336
  name: "generate_image",
@@ -29066,12 +29356,16 @@ function apply(ctx, config = {}) {
29066
29356
  "grok-sub",
29067
29357
  "google-sub"
29068
29358
  ],
29069
- 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."
29359
+ 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."
29070
29360
  },
29071
29361
  model: {
29072
29362
  type: "string",
29073
29363
  description: "Optional model name for this call only, overriding the configured model. Not used by ComfyUI (use workflow instead) nor by the subscription providers (model fixed by the subscription)."
29074
29364
  },
29365
+ channel: {
29366
+ type: "string",
29367
+ description: "Optional configured OpenAI-compatible channel id for this call only. Only meaningful with provider=openai-compat; omit to use the active channel."
29368
+ },
29075
29369
  aspect_ratio: {
29076
29370
  type: "string",
29077
29371
  enum: [
@@ -29095,11 +29389,11 @@ function apply(ctx, config = {}) {
29095
29389
  "2K",
29096
29390
  "4K"
29097
29391
  ],
29098
- description: "Optional output resolution for Google Gemini, xAI Grok, Grok subscription (1K/2K), or Google subscription (1K/4K)."
29392
+ 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."
29099
29393
  },
29100
29394
  size: {
29101
29395
  type: "string",
29102
- description: "Optional dimensions or size tier for OpenAI, Seedream, or DashScope; WIDTHxHEIGHT maps to aspect_ratio for xAI Grok."
29396
+ 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."
29103
29397
  },
29104
29398
  quality: {
29105
29399
  type: "string",
@@ -29141,12 +29435,16 @@ function apply(ctx, config = {}) {
29141
29435
  "grok-sub",
29142
29436
  "google-sub"
29143
29437
  ],
29144
- description: "Optional provider for this call only, applied to every item; omit to use the configured default."
29438
+ 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."
29145
29439
  },
29146
29440
  model: {
29147
29441
  type: "string",
29148
29442
  description: "Optional model name for this call only, applied to every item."
29149
29443
  },
29444
+ channel: {
29445
+ type: "string",
29446
+ description: "Optional configured OpenAI-compatible channel id applied to every item; omit to use the active channel."
29447
+ },
29150
29448
  aspect_ratio: {
29151
29449
  type: "string",
29152
29450
  enum: [
@@ -29170,7 +29468,7 @@ function apply(ctx, config = {}) {
29170
29468
  "2K",
29171
29469
  "4K"
29172
29470
  ],
29173
- description: "Optional output resolution for Google Gemini, xAI Grok, Grok subscription (1K/2K), or Google subscription (1K/4K)."
29471
+ 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."
29174
29472
  },
29175
29473
  size: {
29176
29474
  type: "string",
@@ -29242,6 +29540,7 @@ function apply(ctx, config = {}) {
29242
29540
  type: "string",
29243
29541
  required: true
29244
29542
  },
29543
+ channel: { type: "string" },
29245
29544
  model: {
29246
29545
  type: "string",
29247
29546
  required: true
@@ -29314,6 +29613,7 @@ function apply(ctx, config = {}) {
29314
29613
  prompt,
29315
29614
  ...args.provider !== void 0 ? { provider: args.provider } : {},
29316
29615
  ...args.model !== void 0 ? { model: args.model } : {},
29616
+ ...args.channel !== void 0 ? { channel: args.channel } : {},
29317
29617
  ...args.aspect_ratio !== void 0 ? { aspect_ratio: args.aspect_ratio } : {},
29318
29618
  ...args.image_size !== void 0 ? { image_size: args.image_size } : {},
29319
29619
  ...args.size !== void 0 ? { size: args.size } : {},
@@ -29363,12 +29663,16 @@ function apply(ctx, config = {}) {
29363
29663
  "grok-sub",
29364
29664
  "google-sub"
29365
29665
  ],
29366
- 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."
29666
+ 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."
29367
29667
  },
29368
29668
  model: {
29369
29669
  type: "string",
29370
29670
  description: "Optional model name for this call only, overriding the configured model. Not used by ComfyUI (use workflow instead) nor by the subscription providers (model fixed by the subscription)."
29371
29671
  },
29672
+ channel: {
29673
+ type: "string",
29674
+ description: "Optional configured OpenAI-compatible channel id for this call only. Only meaningful with provider=openai-compat; omit to use the active channel."
29675
+ },
29372
29676
  source: {
29373
29677
  type: "string",
29374
29678
  enum: ["canvas_selection"],
@@ -29415,7 +29719,7 @@ function apply(ctx, config = {}) {
29415
29719
  "2K",
29416
29720
  "4K"
29417
29721
  ],
29418
- description: "Optional output resolution for Google Gemini, xAI Grok, Grok subscription (1K/2K), or Google subscription (1K/4K)."
29722
+ 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."
29419
29723
  },
29420
29724
  size: {
29421
29725
  type: "string",
@@ -29431,8 +29735,10 @@ function apply(ctx, config = {}) {
29431
29735
  }
29432
29736
  },
29433
29737
  output: imageOutput("Edited"),
29434
- async execute(args, exec) {
29435
- const active = resolveProvider(withProviderOverrides(current(), providerOverrideOf(args.provider), args.model));
29738
+ async execute(rawArgs, exec) {
29739
+ const args = cleanImageToolArgs(rawArgs);
29740
+ const requestConfig = configForToolCall(current(), providerOverrideOf(args.provider), args.channel);
29741
+ const active = resolveProvider(withProviderOverrides(requestConfig, providerOverrideOf(args.provider), args.model, "edit"));
29436
29742
  const canvasSelection = args.source === "canvas_selection";
29437
29743
  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");
29438
29744
  const sourceImages = canvasSelection ? await resolveCanvasSelectionReferences({
@@ -29486,7 +29792,7 @@ function apply(ctx, config = {}) {
29486
29792
  signal: exec.signal
29487
29793
  }), active.provider, active.model, "subscription edit", current(), exec, knownWorkspaceRoots);
29488
29794
  }
29489
- const credential = await requireApiKey(ctx, active.provider, "edit_image");
29795
+ const credential = await requireApiKey(ctx, active.provider, "edit_image", active.provider === "openai-compat" ? resolveOpenAICompatChannel(requestConfig, args.channel).keyRef : void 0);
29490
29796
  if (active.provider === "google") {
29491
29797
  const aspectRatio = args.aspect_ratio ?? active.aspectRatio;
29492
29798
  const imageSize = args.image_size ?? active.imageSize;
@@ -29506,14 +29812,14 @@ function apply(ctx, config = {}) {
29506
29812
  const size = args.size ?? active.imageSize;
29507
29813
  if (active.provider === "openai" || active.provider === "openai-compat" || active.provider === "xai" || active.provider === "zhipu") {
29508
29814
  if (active.provider === "openai") validateOfficialSize(active.model, size);
29509
- const quality = active.provider === "openai-compat" ? compatRequestQuality(current(), args.quality) : active.provider === "openai" ? args.quality : void 0;
29815
+ const quality = active.provider === "openai-compat" ? compatRequestQuality(requestConfig, args.quality) : active.provider === "openai" ? args.quality : void 0;
29510
29816
  if (active.provider === "openai" && quality !== void 0 && !officialQualityOptions(active.model).some((option) => option.value === quality)) throw new Error("该 OpenAI 模型不支持所选生成质量");
29511
29817
  const xaiExtraBody = active.provider === "xai" ? xaiToolParameters({
29512
29818
  ...args.aspect_ratio !== void 0 ? { aspectRatio: args.aspect_ratio } : {},
29513
29819
  ...args.image_size !== void 0 ? { imageSize: args.image_size } : {},
29514
29820
  ...args.size !== void 0 ? { size: args.size } : {}
29515
29821
  }) : void 0;
29516
- return saveGenerated(ctx, await editOpenAICompatibleImage({
29822
+ const value = await saveGenerated(ctx, await editOpenAICompatibleImage({
29517
29823
  apiKey: credential,
29518
29824
  baseURL: active.baseURL,
29519
29825
  model: active.model,
@@ -29526,12 +29832,16 @@ function apply(ctx, config = {}) {
29526
29832
  ...active.provider === "openai-compat" ? {
29527
29833
  editFormat: active.editFormat,
29528
29834
  editExtra: active.editExtra,
29529
- editSizeMode: current().openaiCompatEditSizeMode ?? "auto"
29835
+ editSizeMode: requestConfig.openaiCompatEditSizeMode ?? "auto"
29530
29836
  } : active.provider === "xai" ? {
29531
29837
  editFormat: "xaiJson",
29532
29838
  ...xaiExtraBody === void 0 ? {} : { extraBody: xaiExtraBody }
29533
29839
  } : {}
29534
29840
  }), active.provider, active.model, active.provider === "xai" ? xaiExtraBody?.aspect_ratio ?? "auto" : quality === void 0 ? size : `${size}, quality=${quality}`, current(), exec, knownWorkspaceRoots);
29841
+ return active.provider === "openai-compat" ? {
29842
+ ...value,
29843
+ channel: resolveOpenAICompatChannel(requestConfig, args.channel).id
29844
+ } : value;
29535
29845
  }
29536
29846
  if (active.provider === "seedream") return saveGenerated(ctx, await editSeedreamImage({
29537
29847
  apiKey: credential,
@@ -29696,6 +30006,7 @@ function imageOutput(verb) {
29696
30006
  type: "string",
29697
30007
  required: true
29698
30008
  },
30009
+ channel: { type: "string" },
29699
30010
  model: {
29700
30011
  type: "string",
29701
30012
  required: true
@@ -29726,6 +30037,7 @@ function imageOutput(verb) {
29726
30037
  provider: value.provider,
29727
30038
  model: value.model,
29728
30039
  output: value.output,
30040
+ ...value.channel === void 0 ? {} : { channel: value.channel },
29729
30041
  ...verb === "Edited" ? { operation: "edit" } : {},
29730
30042
  ...typeof value.savedTo === "string" ? { savedTo: value.savedTo } : {},
29731
30043
  ...typeof value.seed === "number" ? { seed: value.seed } : {},