dsh-image-gen 0.9.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.en.md CHANGED
@@ -333,7 +333,7 @@ Bring private image generation on your local GPU directly into Agent conversatio
333
333
  | Google subscription | auto · 1:1 · 3:2 · 2:3 · 4:3 · 3:4 · 4:5 · 5:4 · 16:9 · 9:16 · 21:9 | standard / HD (4K) |
334
334
 
335
335
  > An `auto` ratio lets the model pick the framing from the prompt; tiers the upstream dropped (e.g. Seedream 5.0's 1K) are never exposed.
336
- > Google subscription discovers account projects with valid platform enums and includes the ordinary daily endpoint as a compatibility fallback. If discovery returns no project, the legacy compatibility project is still tried but is not cached as an actual account project. Generation failures retain redacted discovery, eligibility, and per-endpoint diagnostics instead of only the last 429. The subscription “Test connection” checks stored sign-in status; successful generation verifies image access for that account. A Code Assist `UNSUPPORTED_CLIENT` response alone does not establish that image generation is unavailable.
336
+ > Google subscription discovers account projects with valid platform enums and includes the ordinary daily endpoint as a compatibility fallback. When discovery returns no project with `UNSUPPORTED_CLIENT`, it retries using the Antigravity client identity without automatically onboarding the account. If discovery still returns no project, the legacy compatibility project is still tried but is not cached as an actual account project. Generation failures retain redacted discovery, eligibility, and per-endpoint diagnostics instead of only the last 429. The subscription “Test connection” checks stored sign-in status; successful generation verifies image access for that account. A Code Assist `UNSUPPORTED_CLIENT` response alone does not establish that image generation is unavailable.
337
337
  > Studio exposes separate aspect ratio, resolution, and generation quality controls for OpenAI and compatible providers. Resolution labels show pixel dimensions. Comparison and regeneration preserve these separate parameters.
338
338
  > Official OpenAI size presets can be customized with `openaiSizes`, shaped as ratio → tier → pixels, e.g. `{ "16:9": { "2K": "2048x1152", "4K": "3840x2160" } }`. For the gpt-image-2 family, dimensions must be multiples of 16, the longest edge at most 3840, aspect ratio within 1:3–3:1, and total pixels within 655360–8294400. Outputs above 3686400 pixels have experimental upstream support. See the [official API reference](https://developers.openai.com/api/reference/resources/images/methods/generate). Older GPT Image models retain their three standard sizes.
339
339
  > Compatible providers use `openaiCompatSizes` in the same format; an empty table retains the legacy sizes. `openaiCompatQuality` sets the default generation quality (empty means omitted), and `openaiCompatQualityOptions` sets Studio choices. Choosing “Provider default (omit)” omits quality for that request. Conversation tools also accept independent `size` and `quality` parameters.
package/README.md CHANGED
@@ -333,7 +333,7 @@ pnpm dsh plugin --profile web add ./dsh-image-gen
333
333
  | Google 订阅 | 自动 · 1:1 · 3:2 · 2:3 · 4:3 · 3:4 · 4:5 · 5:4 · 16:9 · 9:16 · 21:9 | 标准 / 高清(4K) |
334
334
 
335
335
  > 「自动」比例表示由模型按 Prompt 自行决定构图;上游已移除的档位(如 Seedream 5.0 的 1K)插件不会暴露。
336
- > Google 订阅会使用有效的平台枚举发现账号项目,并提供普通 daily 入口作为兼容回退。未返回项目时保留旧兼容项目尝试,但不会把它缓存成账号真实项目;若生图仍失败,错误会保留项目发现、账号资格及各生图入口的脱敏响应,避免只显示最后一个 429。设置中的订阅「测试连接」只检查登录记录,实际生图成功才验证该账号的生图链路;`UNSUPPORTED_CLIENT` 的 Code Assist 提示也不单独判定生图不可用。
336
+ > Google 订阅会使用有效的平台枚举发现账号项目,并提供普通 daily 入口作为兼容回退。项目发现未返回项目且提示 `UNSUPPORTED_CLIENT` 时,会再用 Antigravity 客户端身份重试;该过程不自动开户。仍未返回项目时保留旧兼容项目尝试,但不会把它缓存成账号真实项目;若生图仍失败,错误会保留项目发现、账号资格及各生图入口的脱敏响应,避免只显示最后一个 429。设置中的订阅「测试连接」只检查登录记录,实际生图成功才验证该账号的生图链路;`UNSUPPORTED_CLIENT` 的 Code Assist 提示也不单独判定生图不可用。
337
337
  > OpenAI 与兼容渠道的 Studio 控件分别显示「比例 / 分辨率 / 生成质量」,分辨率选项会显示实际像素。多模型对比及再次生成也保留这两类参数。
338
338
  > OpenAI 官方可通过设置中的 `openaiSizes` 自定义尺寸表,格式为「比例 → 档位 → 像素」,例如 `{ "16:9": { "2K": "2048x1152", "4K": "3840x2160" } }`。gpt-image-2 系列按官方限制校验:边长为 16 的倍数、最大 3840、比例在 1:3 到 3:1、总像素 655360–8294400;超过 3686400 像素的输出属于上游实验支持。[官方说明](https://developers.openai.com/api/reference/resources/images/methods/generate)。旧版 GPT Image 模型仍只使用三种标准尺寸。
339
339
  > OpenAI Compatible 的尺寸表为 `openaiCompatSizes`,格式相同;留空时保持原来的标准尺寸。`openaiCompatQuality` 设置默认生成质量(留空不发送),`openaiCompatQualityOptions` 设置 Studio 可选值。选择「渠道默认(不发送)」可在单次请求中省略 quality;对话工具也支持独立的 `size` 和 `quality` 参数。
package/lib/client.js CHANGED
@@ -125221,7 +125221,7 @@ body[data-ds-dark-theme] .dsh-ig-workbench{--ig-danger:#f08c8c;--ig-warn:#e5b76a
125221
125221
  `;
125222
125222
  //#endregion
125223
125223
  //#region lib/types/client/index.js
125224
- console.info(`[dsh-image-gen] canvas bundle 2026-10-13T08:42:18.331Z`);
125224
+ console.info(`[dsh-image-gen] canvas bundle 2026-10-15T09:51:10.196Z`);
125225
125225
  /** Right-sidebar tab-type registration id (DSH 0.1.5 `sidebarRightTabs`). */
125226
125226
  const SIDEBAR_STUDIO_TAB_ID = "dsh-image-gen/studio";
125227
125227
  /** The tab kind: a page type opened by kind, recognizing no resource address. */
package/lib/index.js CHANGED
@@ -7382,6 +7382,81 @@ async function readBoundedBytes$2(response, maxBytes) {
7382
7382
  return joined;
7383
7383
  }
7384
7384
  //#endregion
7385
+ //#region lib/types/fetch-diagnostics.js
7386
+ /**
7387
+ * Transport-failure diagnostics for provider fetches (#79).
7388
+ *
7389
+ * Node's undici reports the real reason behind a failed fetch (socket reset,
7390
+ * DNS, TLS, ...) only on `error.cause`, and the host's tool-error rendering
7391
+ * keeps just `.message` — so the conversation sees nothing but undici's bare
7392
+ * "fetch failed". These helpers rethrow transport failures with the failing
7393
+ * phase, elapsed seconds, and a bounded cause chain.
7394
+ */
7395
+ /** Cap a single transport-failure description so diagnostics cannot flood the conversation. */
7396
+ const TRANSPORT_DETAIL_LIMIT = 600;
7397
+ /** How deep to walk `error.cause` chains; undici nests at most twice in practice. */
7398
+ const CAUSE_CHAIN_DEPTH = 3;
7399
+ /** Keep body reads tied to the same request timing, cancellation, and secrets. */
7400
+ const responseContexts = /* @__PURE__ */ new WeakMap();
7401
+ /**
7402
+ * Render one error plus its `cause` chain as a single line, e.g.
7403
+ * `TypeError: fetch failed <- Error [UND_ERR_SOCKET]: other side closed`.
7404
+ */
7405
+ function describeErrorChain(error, depth = 0) {
7406
+ if (depth >= CAUSE_CHAIN_DEPTH) return "...";
7407
+ if (error === void 0 || error === null) return "unknown transport error";
7408
+ if (typeof error === "object") {
7409
+ const details = error;
7410
+ if (error instanceof Error || typeof details.message === "string") {
7411
+ const label = [typeof details.name === "string" ? details.name : "Error", typeof details.code === "string" && details.code.length > 0 ? `[${details.code}]` : ""].filter((part) => part.length > 0).join(" ");
7412
+ const message = typeof details.message === "string" ? details.message : "";
7413
+ const text = label.length > 0 && message.length > 0 ? `${label}: ${message}` : label || message;
7414
+ return details.cause !== void 0 && details.cause !== null && depth + 1 < CAUSE_CHAIN_DEPTH ? `${text} <- ${describeErrorChain(details.cause, depth + 1)}` : text || "unknown transport error";
7415
+ }
7416
+ if (typeof details.kind === "string") return details.reason === void 0 || details.reason === null ? details.kind : `${details.kind}: ${describeErrorChain(details.reason, depth + 1)}`;
7417
+ }
7418
+ return String(error).trim() || "unknown transport error";
7419
+ }
7420
+ /** One line describing why a provider fetch failed: the abort reason when the caller cancelled, the cause chain otherwise. */
7421
+ function describeTransportFailure(error, signal) {
7422
+ try {
7423
+ if (signal?.aborted === true) return `aborted (${describeErrorChain(signal.reason ?? error)})`;
7424
+ return describeErrorChain(error);
7425
+ } catch {
7426
+ return "unknown transport error";
7427
+ }
7428
+ }
7429
+ async function withTransportDiagnostics(context, run) {
7430
+ try {
7431
+ return await run();
7432
+ } catch (error) {
7433
+ const elapsed = ((performance.now() - context.startedAt) / 1e3).toFixed(1);
7434
+ const detail = redactSecrets(describeTransportFailure(error, context.signal), ...context.secrets).replace(/\s+/g, " ").slice(0, TRANSPORT_DETAIL_LIMIT);
7435
+ throw new Error(`${context.label} failed after ${elapsed}s: ${detail}`, { cause: error });
7436
+ }
7437
+ }
7438
+ /**
7439
+ * Await one provider fetch and rethrow transport failures as
7440
+ * `${label} failed after 12.3s: ${cause chain}`. The original error stays
7441
+ * reachable on `cause` for host-side error rendering.
7442
+ */
7443
+ async function guardedFetch(label, signal, run, ...secrets) {
7444
+ const context = {
7445
+ label,
7446
+ signal,
7447
+ secrets,
7448
+ startedAt: performance.now()
7449
+ };
7450
+ const response = await withTransportDiagnostics(context, run);
7451
+ responseContexts.set(response, context);
7452
+ return response;
7453
+ }
7454
+ /** Guard only body I/O; parsing, HTTP checks and size enforcement stay with the adapter. */
7455
+ function readResponseBody(response, read) {
7456
+ const context = responseContexts.get(response);
7457
+ return context === void 0 ? read() : withTransportDiagnostics(context, read);
7458
+ }
7459
+ //#endregion
7385
7460
  //#region lib/types/dashscope.js
7386
7461
  async function generateDashScopeImage(options) {
7387
7462
  assertQwenImageModel(options.model);
@@ -7435,7 +7510,7 @@ function toDataUrl$3(image) {
7435
7510
  }
7436
7511
  async function requestQwenImage(options) {
7437
7512
  const base = options.endpoint.replace(/\/+$/, "");
7438
- const response = await fetch(`${base}/services/aigc/multimodal-generation/generation`, {
7513
+ const response = await guardedFetch(`DashScope image ${options.operation}`, options.signal, () => fetch(`${base}/services/aigc/multimodal-generation/generation`, {
7439
7514
  method: "POST",
7440
7515
  ...options.signal ? { signal: options.signal } : {},
7441
7516
  headers: {
@@ -7443,12 +7518,12 @@ async function requestQwenImage(options) {
7443
7518
  authorization: `Bearer ${options.apiKey}`
7444
7519
  },
7445
7520
  body: JSON.stringify(options.requestBody)
7446
- });
7521
+ }), options.apiKey);
7447
7522
  if (!response.ok) {
7448
- const errorText = redactSecrets(await response.text(), options.apiKey);
7523
+ const errorText = redactSecrets(await readResponseBody(response, () => response.text()), options.apiKey);
7449
7524
  throw new Error(`DashScope image ${options.operation} failed (${String(response.status)}): ${errorText}`);
7450
7525
  }
7451
- const payload = await response.json();
7526
+ const payload = JSON.parse(await readResponseBody(response, () => response.text()));
7452
7527
  const imageUrl = extractImageUrl(payload);
7453
7528
  if (imageUrl === void 0) throw new Error(`DashScope image ${options.operation} returned no image URL: ${redactSecrets(payload.message ?? JSON.stringify(payload), options.apiKey)}`);
7454
7529
  return downloadImageBlob(imageUrl, options);
@@ -7463,9 +7538,9 @@ function extractImageUrl(response) {
7463
7538
  }
7464
7539
  }
7465
7540
  async function downloadImageBlob(imageUrl, options) {
7466
- const imageResponse = await fetch(imageUrl, { ...options.signal ? { signal: options.signal } : {} });
7541
+ const imageResponse = await guardedFetch("DashScope image download", options.signal, () => fetch(imageUrl, { ...options.signal ? { signal: options.signal } : {} }), options.apiKey);
7467
7542
  if (!imageResponse.ok) throw new Error(`Failed to fetch DashScope image from URL (${String(imageResponse.status)})`);
7468
- const buffer = await imageResponse.arrayBuffer();
7543
+ const buffer = await readResponseBody(imageResponse, () => imageResponse.arrayBuffer());
7469
7544
  if (buffer.byteLength > options.maxBytes) throw new Error(`DashScope generated image (${String(buffer.byteLength)} bytes) exceeds the ${String(options.maxBytes)} byte limit`);
7470
7545
  const data = new Uint8Array(buffer);
7471
7546
  const mediaType = detectImageMediaType(data) ?? imageMediaType$3(imageResponse.headers.get("content-type"));
@@ -7520,7 +7595,7 @@ function editGoogleImage(input) {
7520
7595
  /** Shared Google request, response parsing, decoding, and size enforcement. */
7521
7596
  async function requestGoogleImage(input) {
7522
7597
  const label = `Google image ${input.operation}`;
7523
- const response = await fetch(input.endpoint, {
7598
+ const response = await guardedFetch(`${label} request`, input.signal, () => fetch(input.endpoint, {
7524
7599
  method: "POST",
7525
7600
  redirect: "error",
7526
7601
  signal: input.signal,
@@ -7538,7 +7613,7 @@ async function requestGoogleImage(input) {
7538
7613
  image_size: input.imageSize
7539
7614
  }
7540
7615
  })
7541
- });
7616
+ }), input.apiKey);
7542
7617
  const text = await readBoundedText$2(response, Math.ceil(input.maxBytes * 1.4) + ERROR_LIMIT$2, label);
7543
7618
  if (!response.ok) throw new Error(`${label} failed (${response.status}): ${redactSecrets(text, input.apiKey).slice(0, ERROR_LIMIT$2)}`);
7544
7619
  let payload;
@@ -7601,7 +7676,7 @@ async function readBoundedText$2(response, maxBytes, label) {
7601
7676
  let bytes = 0;
7602
7677
  try {
7603
7678
  for (;;) {
7604
- const next = await reader.read();
7679
+ const next = await readResponseBody(response, () => reader.read());
7605
7680
  if (next.done) break;
7606
7681
  bytes += next.value.byteLength;
7607
7682
  if (bytes > maxBytes) throw new Error(`${label} response exceeded the ${String(maxBytes)} byte limit`);
@@ -8101,7 +8176,7 @@ function jsonError$4(res, status, code) {
8101
8176
  //#region lib/types/openai-compatible.js
8102
8177
  const ERROR_LIMIT$1 = 4096;
8103
8178
  async function generateOpenAICompatibleImage(input) {
8104
- return parseImageResponse(await fetch(imageEndpoint$1(input.baseURL, "generations"), {
8179
+ return parseImageResponse(await guardedFetch(`${input.provider} image request`, input.signal, () => fetch(imageEndpoint$1(input.baseURL, "generations"), {
8105
8180
  method: "POST",
8106
8181
  redirect: "error",
8107
8182
  signal: input.signal,
@@ -8120,10 +8195,10 @@ async function generateOpenAICompatibleImage(input) {
8120
8195
  } : {},
8121
8196
  ...input.extraBody ?? {}
8122
8197
  })
8123
- }), input.provider, input);
8198
+ }), input.apiKey), input.provider, input);
8124
8199
  }
8125
8200
  async function editOpenAICompatibleImage(input) {
8126
- if (input.editFormat === "qwenJson") return parseImageResponse(await fetch(imageEndpoint$1(input.baseURL, "generations"), {
8201
+ if (input.editFormat === "qwenJson") return parseImageResponse(await guardedFetch("openai-compat image edit request", input.signal, () => fetch(imageEndpoint$1(input.baseURL, "generations"), {
8127
8202
  method: "POST",
8128
8203
  redirect: "error",
8129
8204
  signal: input.signal,
@@ -8140,13 +8215,13 @@ async function editOpenAICompatibleImage(input) {
8140
8215
  ...input.editExtra ?? {},
8141
8216
  ...input.extraBody ?? {}
8142
8217
  })
8143
- }), "openai-compat", input);
8218
+ }), input.apiKey), "openai-compat", input);
8144
8219
  if (input.editFormat === "xaiJson") {
8145
8220
  const references = input.sourceImages.map((image) => ({
8146
8221
  type: "image_url",
8147
8222
  url: toDataUrl$2(image)
8148
8223
  }));
8149
- return parseImageResponse(await fetch(imageEndpoint$1(input.baseURL, "edits"), {
8224
+ return parseImageResponse(await guardedFetch("xai image edit request", input.signal, () => fetch(imageEndpoint$1(input.baseURL, "edits"), {
8150
8225
  method: "POST",
8151
8226
  redirect: "error",
8152
8227
  signal: input.signal,
@@ -8161,7 +8236,7 @@ async function editOpenAICompatibleImage(input) {
8161
8236
  ...input.quality === void 0 ? {} : { quality: input.quality },
8162
8237
  ...input.extraBody ?? {}
8163
8238
  })
8164
- }), "xai", input);
8239
+ }), input.apiKey), "xai", input);
8165
8240
  }
8166
8241
  if (input.editFormat === "jsonImageUrlArray") {
8167
8242
  const body = {
@@ -8175,7 +8250,7 @@ async function editOpenAICompatibleImage(input) {
8175
8250
  ...input.editExtra ?? {},
8176
8251
  ...input.extraBody ?? {}
8177
8252
  };
8178
- return parseImageResponse(await fetch(imageEndpoint$1(input.baseURL, "edits"), {
8253
+ return parseImageResponse(await guardedFetch("openai image edit request", input.signal, () => fetch(imageEndpoint$1(input.baseURL, "edits"), {
8179
8254
  method: "POST",
8180
8255
  redirect: "error",
8181
8256
  signal: input.signal,
@@ -8184,7 +8259,7 @@ async function editOpenAICompatibleImage(input) {
8184
8259
  "content-type": "application/json"
8185
8260
  },
8186
8261
  body: JSON.stringify(body)
8187
- }), "openai", input);
8262
+ }), input.apiKey), "openai", input);
8188
8263
  }
8189
8264
  if (input.editFormat === "formReferenceImages") {
8190
8265
  const form = new FormData();
@@ -8195,13 +8270,13 @@ async function editOpenAICompatibleImage(input) {
8195
8270
  for (const [key, value] of Object.entries(input.extraBody ?? {})) form.append(key, String(value));
8196
8271
  form.append("response_format", "b64_json");
8197
8272
  form.append("reference_images", JSON.stringify(input.sourceImages.map((sourceImage) => Buffer.from(sourceImage.data).toString("base64"))));
8198
- return parseImageResponse(await fetch(imageEndpoint$1(input.baseURL, "edits"), {
8273
+ return parseImageResponse(await guardedFetch("openai image edit request", input.signal, () => fetch(imageEndpoint$1(input.baseURL, "edits"), {
8199
8274
  method: "POST",
8200
8275
  redirect: "error",
8201
8276
  signal: input.signal,
8202
8277
  headers: { authorization: `Bearer ${input.apiKey}` },
8203
8278
  body: form
8204
- }), "openai", input);
8279
+ }), input.apiKey), "openai", input);
8205
8280
  }
8206
8281
  const form = new FormData();
8207
8282
  const imageField = input.sourceImages.length > 1 ? "image[]" : "image";
@@ -8216,13 +8291,13 @@ async function editOpenAICompatibleImage(input) {
8216
8291
  if (input.size !== void 0 && input.size.length > 0) form.append("size", input.size);
8217
8292
  if (input.quality !== void 0) form.append("quality", input.quality);
8218
8293
  for (const [key, value] of Object.entries(input.extraBody ?? {})) form.append(key, String(value));
8219
- return parseImageResponse(await fetch(imageEndpoint$1(input.baseURL, "edits"), {
8294
+ return parseImageResponse(await guardedFetch("openai image edit request", input.signal, () => fetch(imageEndpoint$1(input.baseURL, "edits"), {
8220
8295
  method: "POST",
8221
8296
  redirect: "error",
8222
8297
  signal: input.signal,
8223
8298
  headers: { authorization: `Bearer ${input.apiKey}` },
8224
8299
  body: form
8225
- }), "openai", input);
8300
+ }), input.apiKey), "openai", input);
8226
8301
  }
8227
8302
  async function parseImageResponse(response, provider, input) {
8228
8303
  const text = await readBoundedText$1(response, Math.ceil(input.maxBytes * 1.4) + ERROR_LIMIT$1);
@@ -8282,15 +8357,15 @@ async function downloadImage$1(url, provider, input) {
8282
8357
  mediaType: detectImageMediaType(data) ?? imageMediaType$2(parsed.mediaType) ?? "image/png"
8283
8358
  };
8284
8359
  }
8285
- let response = await fetch(url, {
8360
+ let response = await guardedFetch(`${provider} image download`, input.signal, () => fetch(url, {
8286
8361
  redirect: "follow",
8287
8362
  signal: input.signal,
8288
8363
  ...input.apiKey === void 0 ? {} : { headers: { authorization: `Bearer ${input.apiKey}` } }
8289
- });
8290
- if (!response.ok && input.apiKey !== void 0) response = await fetch(url, {
8364
+ }), input.apiKey);
8365
+ if (!response.ok && input.apiKey !== void 0) response = await guardedFetch(`${provider} image download`, input.signal, () => fetch(url, {
8291
8366
  redirect: "follow",
8292
8367
  signal: input.signal
8293
- });
8368
+ }), input.apiKey);
8294
8369
  if (!response.ok) throw new Error(`${provider} image download failed (${response.status})`);
8295
8370
  const data = await readBoundedBytes$1(response, input.maxBytes);
8296
8371
  const mediaType = detectImageMediaType(data) ?? imageMediaType$2(response.headers.get("content-type"));
@@ -8336,7 +8411,7 @@ async function readBoundedBytes$1(response, maxBytes) {
8336
8411
  let bytes = 0;
8337
8412
  try {
8338
8413
  for (;;) {
8339
- const next = await reader.read();
8414
+ const next = await readResponseBody(response, () => reader.read());
8340
8415
  if (next.done) break;
8341
8416
  bytes += next.value.byteLength;
8342
8417
  if (bytes > maxBytes) throw new Error(`Image response exceeded the ${String(maxBytes)} byte limit`);
@@ -8358,7 +8433,7 @@ async function readBoundedBytes$1(response, maxBytes) {
8358
8433
  const ERROR_LIMIT = 4096;
8359
8434
  /** Edit one image through Ark ImageGenerations using a data-URL reference. */
8360
8435
  async function editSeedreamImage(input) {
8361
- const response = await fetch(imageEndpoint(input.baseURL), {
8436
+ const response = await guardedFetch("seedream image editing", input.signal, () => fetch(imageEndpoint(input.baseURL), {
8362
8437
  method: "POST",
8363
8438
  redirect: "error",
8364
8439
  signal: input.signal,
@@ -8374,7 +8449,7 @@ async function editSeedreamImage(input) {
8374
8449
  ...arkOutputBody(input.arkOptions),
8375
8450
  response_format: "b64_json"
8376
8451
  })
8377
- });
8452
+ }), input.apiKey);
8378
8453
  const text = await readBoundedText(response, Math.ceil(input.maxBytes * 1.4) + ERROR_LIMIT);
8379
8454
  if (!response.ok) throw new Error(`seedream image editing failed (${response.status}): ${redactSecrets(text, input.apiKey).slice(0, ERROR_LIMIT)}`);
8380
8455
  let payload;
@@ -8423,10 +8498,10 @@ function firstImage(value) {
8423
8498
  }
8424
8499
  async function downloadImage(url, input) {
8425
8500
  if (url === void 0) throw new Error("seedream image editing returned no image data");
8426
- const response = await fetch(url, {
8501
+ const response = await guardedFetch("seedream image download", input.signal, () => fetch(url, {
8427
8502
  redirect: "follow",
8428
8503
  signal: input.signal
8429
- });
8504
+ }), input.apiKey);
8430
8505
  if (!response.ok) throw new Error(`seedream image download failed (${response.status})`);
8431
8506
  const mediaType = imageMediaType$1(response.headers.get("content-type"));
8432
8507
  if (mediaType === void 0) throw new Error("seedream image download returned unsupported content type");
@@ -8456,7 +8531,7 @@ async function readBoundedBytes(response, maxBytes) {
8456
8531
  let bytes = 0;
8457
8532
  try {
8458
8533
  for (;;) {
8459
- const next = await reader.read();
8534
+ const next = await readResponseBody(response, () => reader.read());
8460
8535
  if (next.done) break;
8461
8536
  bytes += next.value.byteLength;
8462
8537
  if (bytes > maxBytes) throw new Error(`Image response exceeded the ${String(maxBytes)} byte limit`);
@@ -8937,20 +9012,24 @@ function getAntigravityVersion() {
8937
9012
  return antigravityVersion ?? ANTIGRAVITY_VERSION_FALLBACK;
8938
9013
  }
8939
9014
  /** Fetch and cache the current version once; failures keep the fallback. */
8940
- async function initAntigravityVersion() {
9015
+ async function initAntigravityVersion(signal) {
9016
+ signal?.throwIfAborted();
8941
9017
  if (antigravityVersion !== void 0) return;
8942
- const parsed = await fetchVersionText(VERSION_URL) ?? await fetchVersionText(CHANGELOG_URL, CHANGELOG_SCAN_CHARS);
9018
+ const parsed = await fetchVersionText(VERSION_URL, void 0, signal) ?? await fetchVersionText(CHANGELOG_URL, CHANGELOG_SCAN_CHARS, signal);
9019
+ signal?.throwIfAborted();
8943
9020
  if (parsed !== void 0) antigravityVersion = parsed;
8944
9021
  }
8945
- async function fetchVersionText(url, maxChars) {
9022
+ async function fetchVersionText(url, maxChars, signal) {
8946
9023
  try {
8947
- const response = await fetch(url, { signal: AbortSignal.timeout(VERSION_FETCH_TIMEOUT_MS) });
9024
+ const timeout = AbortSignal.timeout(VERSION_FETCH_TIMEOUT_MS);
9025
+ const response = await fetch(url, { signal: signal === void 0 ? timeout : AbortSignal.any([signal, timeout]) });
8948
9026
  if (!response.ok) return void 0;
8949
9027
  let text = await response.text();
8950
9028
  if (maxChars !== void 0) text = text.slice(0, maxChars);
8951
9029
  const match = text.match(VERSION_REGEX);
8952
9030
  return match === null ? void 0 : match[0];
8953
- } catch {
9031
+ } catch (error) {
9032
+ if (signal?.aborted === true) throw error;
8954
9033
  return;
8955
9034
  }
8956
9035
  }
@@ -9016,16 +9095,19 @@ function antigravityAuthorizeUrl(cfg, pkce) {
9016
9095
  });
9017
9096
  }
9018
9097
  /**
9019
- * Headers for project discovery (loadCodeAssist): the reference keeps the
9020
- * plain SDK identity here, mirroring its project.js loadHeaders.
9098
+ * Preserve the SDK discovery identity first. A no-project UNSUPPORTED_CLIENT
9099
+ * response can be retried with the same Antigravity identity as generation.
9021
9100
  */
9022
- function loadHeaders(projectId) {
9101
+ function loadHeaders(projectId, identity) {
9023
9102
  return {
9024
- "User-Agent": "google-api-nodejs-client/9.15.1",
9103
+ "User-Agent": identity === "antigravity" ? antigravityUserAgent() : "google-api-nodejs-client/9.15.1",
9025
9104
  "X-Goog-Api-Client": "google-cloud-sdk vscode_cloudshelleditor/0.1",
9026
9105
  "Client-Metadata": JSON.stringify(clientMetadata(projectId))
9027
9106
  };
9028
9107
  }
9108
+ function antigravityUserAgent() {
9109
+ return `Mozilla/5.0 (${process.platform === "darwin" ? "Macintosh; Intel Mac OS X 10_15_7" : "Windows NT 10.0; Win64; x64"}) AppleWebKit/537.36 (KHTML, like Gecko) Antigravity/${getAntigravityVersion()} Chrome/138.0.7204.235 Electron/37.3.1 Safari/537.36`;
9110
+ }
9029
9111
  /**
9030
9112
  * Headers for content requests (image generation): the full Electron
9031
9113
  * browser User-Agent with the Antigravity client version, matching the
@@ -9034,7 +9116,7 @@ function loadHeaders(projectId) {
9034
9116
  */
9035
9117
  function contentHeaders(projectId) {
9036
9118
  return {
9037
- "User-Agent": `Mozilla/5.0 (${process.platform === "darwin" ? "Macintosh; Intel Mac OS X 10_15_7" : "Windows NT 10.0; Win64; x64"}) AppleWebKit/537.36 (KHTML, like Gecko) Antigravity/${getAntigravityVersion()} Chrome/138.0.7204.235 Electron/37.3.1 Safari/537.36`,
9119
+ "User-Agent": antigravityUserAgent(),
9038
9120
  "X-Goog-Api-Client": "google-cloud-sdk vscode_cloudshelleditor/0.1",
9039
9121
  "Client-Metadata": JSON.stringify(clientMetadata(projectId))
9040
9122
  };
@@ -9100,40 +9182,50 @@ async function antigravityResolveProjectContext(blob, signal) {
9100
9182
  signal?.throwIfAborted();
9101
9183
  const metadata = clientMetadata("");
9102
9184
  const diagnostics = [];
9103
- for (const base of LOAD_ENDPOINTS) {
9104
- const label = `loadCodeAssist ${new URL(base).hostname}`;
9105
- try {
9106
- const response = await fetch(`${base}/v1internal:loadCodeAssist`, {
9107
- method: "POST",
9108
- headers: {
9109
- "Content-Type": "application/json",
9110
- Authorization: `Bearer ${blob.accessToken}`,
9111
- ...loadHeaders("")
9112
- },
9113
- body: JSON.stringify({ metadata }),
9114
- signal: signal === void 0 ? AbortSignal.timeout(25e3) : AbortSignal.any([signal, AbortSignal.timeout(25e3)])
9115
- });
9116
- if (!response.ok) {
9117
- diagnostics.push(`${label}: ${httpDiagnostic(response.status, await response.text().catch(() => ""), blob)}`);
9118
- continue;
9119
- }
9120
- const payload = recordOf(await response.json().catch(() => null));
9121
- if (payload === void 0) {
9122
- diagnostics.push(`${label}: HTTP ${String(response.status)}, invalid response`);
9123
- continue;
9185
+ const identityRetryEndpoints = [];
9186
+ for (const identity of ["sdk", "antigravity"]) {
9187
+ const endpoints = identity === "sdk" ? LOAD_ENDPOINTS : identityRetryEndpoints;
9188
+ if (identity === "antigravity") {
9189
+ if (endpoints.length === 0) break;
9190
+ await initAntigravityVersion(signal);
9191
+ }
9192
+ for (const base of endpoints) {
9193
+ signal?.throwIfAborted();
9194
+ const label = `loadCodeAssist ${new URL(base).hostname}${identity === "antigravity" ? ` [Antigravity/${getAntigravityVersion()}]` : ""}`;
9195
+ try {
9196
+ const response = await fetch(`${base}/v1internal:loadCodeAssist`, {
9197
+ method: "POST",
9198
+ headers: {
9199
+ "Content-Type": "application/json",
9200
+ Authorization: `Bearer ${blob.accessToken}`,
9201
+ ...loadHeaders("", identity)
9202
+ },
9203
+ body: JSON.stringify({ metadata }),
9204
+ signal: signal === void 0 ? AbortSignal.timeout(25e3) : AbortSignal.any([signal, AbortSignal.timeout(25e3)])
9205
+ });
9206
+ if (!response.ok) {
9207
+ diagnostics.push(`${label}: ${httpDiagnostic(response.status, await response.text().catch(() => ""), blob)}`);
9208
+ continue;
9209
+ }
9210
+ const payload = recordOf(await response.json().catch(() => null));
9211
+ if (payload === void 0) {
9212
+ diagnostics.push(`${label}: HTTP ${String(response.status)}, invalid response`);
9213
+ continue;
9214
+ }
9215
+ const project = payload.cloudaicompanionProject;
9216
+ const projectRecord = recordOf(project);
9217
+ const id = typeof project === "string" ? project : typeof projectRecord?.id === "string" ? projectRecord.id : "";
9218
+ if (id.trim().length > 0) return {
9219
+ projectId: id.trim(),
9220
+ source: "account",
9221
+ diagnostics
9222
+ };
9223
+ diagnostics.push(`${label}: HTTP ${String(response.status)}, ${missingProjectDiagnostic(payload, blob)}`);
9224
+ if (identity === "sdk" && Array.isArray(payload.ineligibleTiers) && payload.ineligibleTiers.some((tier) => recordOf(tier)?.reasonCode === "UNSUPPORTED_CLIENT")) identityRetryEndpoints.push(base);
9225
+ } catch (error) {
9226
+ if (signal?.aborted === true) throw error;
9227
+ diagnostics.push(`${label}: ${diagnosticText(error instanceof Error ? error.message : "request failed", blob)}`);
9124
9228
  }
9125
- const project = payload.cloudaicompanionProject;
9126
- const projectRecord = recordOf(project);
9127
- const id = typeof project === "string" ? project : typeof projectRecord?.id === "string" ? projectRecord.id : "";
9128
- if (id.trim().length > 0) return {
9129
- projectId: id.trim(),
9130
- source: "account",
9131
- diagnostics
9132
- };
9133
- diagnostics.push(`${label}: HTTP ${String(response.status)}, ${missingProjectDiagnostic(payload, blob)}`);
9134
- } catch (error) {
9135
- if (signal?.aborted === true) throw error;
9136
- diagnostics.push(`${label}: ${diagnosticText(error instanceof Error ? error.message : "request failed", blob)}`);
9137
9229
  }
9138
9230
  }
9139
9231
  return {
@@ -9250,7 +9342,7 @@ function antigravityEnvelope(projectId, request) {
9250
9342
  */
9251
9343
  async function antigravityGenerateImage(options) {
9252
9344
  options.signal?.throwIfAborted();
9253
- await initAntigravityVersion();
9345
+ await initAntigravityVersion(options.signal);
9254
9346
  const inner = antigravityImageBody({
9255
9347
  prompt: options.prompt,
9256
9348
  ...options.aspectRatio !== void 0 ? { aspectRatio: options.aspectRatio } : {},
@@ -0,0 +1,10 @@
1
+ /** One line describing why a provider fetch failed: the abort reason when the caller cancelled, the cause chain otherwise. */
2
+ export declare function describeTransportFailure(error: unknown, signal?: AbortSignal): string;
3
+ /**
4
+ * Await one provider fetch and rethrow transport failures as
5
+ * `${label} failed after 12.3s: ${cause chain}`. The original error stays
6
+ * reachable on `cause` for host-side error rendering.
7
+ */
8
+ export declare function guardedFetch(label: string, signal: AbortSignal | undefined, run: () => Promise<Response>, ...secrets: Array<string | undefined>): Promise<Response>;
9
+ /** Guard only body I/O; parsing, HTTP checks and size enforcement stay with the adapter. */
10
+ export declare function readResponseBody<T>(response: Response, read: () => Promise<T>): Promise<T>;
@@ -17,7 +17,7 @@ export declare function getAntigravityVersion(): string;
17
17
  /** Test seam: pin the version so unit tests never depend on the network. */
18
18
  export declare function setAntigravityVersionForTest(version: string | undefined): void;
19
19
  /** Fetch and cache the current version once; failures keep the fallback. */
20
- export declare function initAntigravityVersion(): Promise<void>;
20
+ export declare function initAntigravityVersion(signal?: AbortSignal): Promise<void>;
21
21
  export interface AntigravityConfig {
22
22
  clientId: string;
23
23
  clientSecret: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-image-gen",
3
- "version": "0.9.0",
3
+ "version": "0.9.1",
4
4
  "description": "Bring ChatGPT-like image generation to DeepSeek Harness — Gemini, OpenAI, Seedream, DashScope, local ComfyUI & more.",
5
5
  "type": "module",
6
6
  "author": "shanliuling",