@aexol/spectral 0.9.249 → 0.9.250

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.
@@ -8,11 +8,18 @@
8
8
  * something a provider can actually consume:
9
9
  *
10
10
  * - `data` present -> `data:<mimeType>;base64,<data>` (unchanged, legacy safe)
11
- * - `url` public -> passed through as-is (no base64 in the context!)
12
- * - `url` backend-hosted -> swapped for a presigned, headerless-fetchable
13
- * url (`/generated-images/<id>` is what the
14
- * backend returns; it is AUTH-GATED, so handing
15
- * it to a provider only yields a 401)
11
+ * - `url` public (3rd-party) -> passed through as-is (no base64 in the context!)
12
+ * - `url` backend-hosted -> swapped for a presigned S3 url, which is then
13
+ * ALWAYS downloaded anonymously here and inlined
14
+ * as a base64 data URI — OpenRouter's server-side
15
+ * media fetcher gets HTTP 400 from S3 on
16
+ * presigned urls, so the raw presigned url must
17
+ * never reach the provider. If the S3 download
18
+ * fails, the presigned url is sent as a fallback
19
+ * instead of dropping the image.
20
+ * (`/generated-images/<id>` is what the backend
21
+ * returns; it is AUTH-GATED, so handing it to a
22
+ * provider only yields a 401)
16
23
  * - `url` relative -> resolved against the backend base url first
17
24
  * - `url` local/private -> fetched and inlined **at send time only**
18
25
  *
@@ -80,7 +87,10 @@ export interface ResolvedImageSource {
80
87
  data?: string;
81
88
  /**
82
89
  * True when `url` is a presigned url returned by `resolveHostedUrl` (i.e.
83
- * it replaced an auth-gated `/generated-images/<id>` reference).
90
+ * it replaced an auth-gated `/generated-images/<id>` reference). Only set
91
+ * when the presigned url itself is handed to the provider — which only
92
+ * happens as a fallback when inlining its bytes failed; a successful
93
+ * download is inlined (`inlined: true`) and the flag stays unset.
84
94
  */
85
95
  hostedResolved?: boolean;
86
96
  }
@@ -1 +1 @@
1
- {"version":3,"file":"image-source.d.ts","sourceRoot":"","sources":["../../../../src/sdk/ai/utils/image-source.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"image-source.d.ts","sourceRoot":"","sources":["../../../../src/sdk/ai/utils/image-source.ts"],"names":[],"mappings":"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"}
@@ -8,11 +8,18 @@
8
8
  * something a provider can actually consume:
9
9
  *
10
10
  * - `data` present -> `data:<mimeType>;base64,<data>` (unchanged, legacy safe)
11
- * - `url` public -> passed through as-is (no base64 in the context!)
12
- * - `url` backend-hosted -> swapped for a presigned, headerless-fetchable
13
- * url (`/generated-images/<id>` is what the
14
- * backend returns; it is AUTH-GATED, so handing
15
- * it to a provider only yields a 401)
11
+ * - `url` public (3rd-party) -> passed through as-is (no base64 in the context!)
12
+ * - `url` backend-hosted -> swapped for a presigned S3 url, which is then
13
+ * ALWAYS downloaded anonymously here and inlined
14
+ * as a base64 data URI — OpenRouter's server-side
15
+ * media fetcher gets HTTP 400 from S3 on
16
+ * presigned urls, so the raw presigned url must
17
+ * never reach the provider. If the S3 download
18
+ * fails, the presigned url is sent as a fallback
19
+ * instead of dropping the image.
20
+ * (`/generated-images/<id>` is what the backend
21
+ * returns; it is AUTH-GATED, so handing it to a
22
+ * provider only yields a 401)
16
23
  * - `url` relative -> resolved against the backend base url first
17
24
  * - `url` local/private -> fetched and inlined **at send time only**
18
25
  *
@@ -287,13 +294,26 @@ export async function resolveImageForProvider(image, options = {}) {
287
294
  // the provider would fetch it anonymously and get a 401. Swap it for a
288
295
  // presigned url first (that one needs no headers at all).
289
296
  const hosted = isHostedBackendImageUrl(url, backendUrl);
297
+ // Url the bytes are downloaded from: the presigned url when the hosted
298
+ // swap succeeded, the (possibly rewritten) backend url otherwise.
299
+ let downloadUrl = url;
300
+ let presignedResolved = false;
290
301
  if (hosted && options.resolveHostedUrl) {
291
302
  try {
292
303
  const resolvedUrl = (await options.resolveHostedUrl(url, { signal: options.signal, timeoutMs: options.timeoutMs }))?.trim();
293
304
  if (resolvedUrl && !isLocalOrPrivateImageUrl(resolvedUrl)) {
294
- return { url: resolvedUrl, inlined: false, mimeType: image.mimeType, hostedResolved: true };
305
+ // OpenRouter's server-side media fetcher gets HTTP 400 from S3 on
306
+ // presigned urls (the in-repo vLLM fetcher had the same bug), so
307
+ // the presigned url must NEVER be handed over verbatim. Fall
308
+ // through to the inline path below, which downloads the bytes
309
+ // anonymously here and sends a base64 data URI instead; if that
310
+ // download fails, the presigned url is still sent (the fallback
311
+ // at the bottom keeps today's behavior instead of dropping the
312
+ // image).
313
+ downloadUrl = resolvedUrl;
314
+ presignedResolved = true;
295
315
  }
296
- if (resolvedUrl) {
316
+ if (resolvedUrl && !presignedResolved) {
297
317
  // The backend minted the presigned url on a LOCAL/private endpoint
298
318
  // (dev S3 on `http://localhost:9002`) — a remote provider can never
299
319
  // fetch it. Do NOT hand over that dead url: fall through to the
@@ -316,14 +336,16 @@ export async function resolveImageForProvider(image, options = {}) {
316
336
  }
317
337
  // Only inline a hosted url when we can actually authenticate the download;
318
338
  // without credentials the fetch is a guaranteed 401 anyway, so the plain
319
- // pass-through (identical end result, one request less) is kept.
320
- const authHeaders = hosted ? await resolveAuthHeadersOption(options.authHeaders) : undefined;
339
+ // pass-through (identical end result, one request less) is kept. A
340
+ // presigned url needs NO credentials — it is downloaded anonymously and
341
+ // always inlined.
342
+ const authHeaders = hosted && !presignedResolved ? await resolveAuthHeadersOption(options.authHeaders) : undefined;
321
343
  // Belt and braces: re-check the origin here, because `url` was rewritten
322
344
  // above (relative -> absolute) and only a url that is still ours may carry
323
345
  // credentials. The resolver refuses foreign origins too, this is the last
324
346
  // gate before the token goes on the wire.
325
347
  const authenticated = authHeaders !== undefined && isHostedBackendImageUrl(url, backendUrl);
326
- if (!isLocalOrPrivateImageUrl(url) && !authenticated) {
348
+ if (!isLocalOrPrivateImageUrl(url) && !authenticated && !presignedResolved) {
327
349
  return { url, inlined: false, mimeType: image.mimeType };
328
350
  }
329
351
  const requestHeaders = authenticated ? authHeaders : undefined;
@@ -341,7 +363,7 @@ export async function resolveImageForProvider(image, options = {}) {
341
363
  // `requestHeaders` is only set when the (possibly rewritten) url still
342
364
  // resolves to the backend origin — a url on any other host is fetched
343
365
  // anonymously, so the JWT cannot leak to a third party.
344
- const response = await doFetch(url, requestHeaders ? { signal: controller.signal, headers: requestHeaders } : { signal: controller.signal });
366
+ const response = await doFetch(downloadUrl, requestHeaders ? { signal: controller.signal, headers: requestHeaders } : { signal: controller.signal });
345
367
  if (!response.ok)
346
368
  throw new Error(`HTTP ${response.status}`);
347
369
  // Reject oversized payloads from the declared length BEFORE reading
@@ -368,7 +390,12 @@ export async function resolveImageForProvider(image, options = {}) {
368
390
  }
369
391
  catch (error) {
370
392
  const reason = error instanceof Error ? error.message : String(error);
371
- options.onWarn?.(`Could not inline local image ${url} (${reason}); sending url as-is${hosted ? " — the provider may not be able to fetch it (auth required)" : ""}`);
393
+ options.onWarn?.(`Could not inline image ${presignedResolved ? downloadUrl : url} (${reason}); sending url as-is${hosted && !presignedResolved ? " — the provider may not be able to fetch it (auth required)" : ""}`);
394
+ if (presignedResolved) {
395
+ // The S3 download failed — degrade to the old verbatim behavior
396
+ // (send the presigned url) rather than dropping the image.
397
+ return { url: downloadUrl, inlined: false, mimeType: image.mimeType, hostedResolved: true };
398
+ }
372
399
  return { url, inlined: false, mimeType: image.mimeType };
373
400
  }
374
401
  finally {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aexol/spectral",
3
- "version": "0.9.249",
3
+ "version": "0.9.250",
4
4
  "description": "AI coding agent for Aexol with relay-based browser access.",
5
5
  "type": "module",
6
6
  "private": false,