@juspay/neurolink 10.4.4 → 10.5.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/CHANGELOG.md +12 -0
- package/dist/browser/neurolink.min.js +354 -354
- package/dist/lib/processors/config/mimeConstants.js +2 -0
- package/dist/lib/server/routes/claudeProxyRoutes.d.ts +11 -0
- package/dist/lib/server/routes/claudeProxyRoutes.js +55 -0
- package/dist/lib/utils/fileDetector.js +41 -7
- package/dist/lib/utils/imageProcessor.js +19 -20
- package/dist/lib/utils/isoBmff.d.ts +24 -0
- package/dist/lib/utils/isoBmff.js +83 -0
- package/dist/processors/config/mimeConstants.js +2 -0
- package/dist/server/routes/claudeProxyRoutes.d.ts +11 -0
- package/dist/server/routes/claudeProxyRoutes.js +55 -0
- package/dist/utils/fileDetector.js +41 -7
- package/dist/utils/imageProcessor.js +19 -20
- package/dist/utils/isoBmff.d.ts +24 -0
- package/dist/utils/isoBmff.js +82 -0
- package/package.json +3 -2
|
@@ -283,6 +283,8 @@ export const EXTENSION_MIME_MAP = {
|
|
|
283
283
|
".webp": IMAGE_MIME_TYPES.WEBP,
|
|
284
284
|
".bmp": IMAGE_MIME_TYPES.BMP,
|
|
285
285
|
".ico": IMAGE_MIME_TYPES.ICO,
|
|
286
|
+
".heic": IMAGE_MIME_TYPES.HEIC,
|
|
287
|
+
".heif": IMAGE_MIME_TYPES.HEIF,
|
|
286
288
|
// Source code
|
|
287
289
|
".js": TEXT_MIME_TYPES.JAVASCRIPT,
|
|
288
290
|
".mjs": TEXT_MIME_TYPES.JAVASCRIPT,
|
|
@@ -249,6 +249,17 @@ export declare function parseClaudeErrorBody(errBody: string): ParsedClaudeError
|
|
|
249
249
|
* Detect malformed request errors that should not trigger account/provider failover.
|
|
250
250
|
*/
|
|
251
251
|
export declare function isInvalidRequestError(status: number, errBody: string): boolean;
|
|
252
|
+
/**
|
|
253
|
+
* A subscription-specific beta rejection. Anthropic returns
|
|
254
|
+
* `400 invalid_request_error` with a message like
|
|
255
|
+
* "The long context beta is not yet available for this subscription." when an
|
|
256
|
+
* account's plan tier does not grant an optional beta the proxy injected (see
|
|
257
|
+
* CLAUDE_CODE_OAUTH_BETAS in anthropicOAuth.ts). Unlike a genuinely malformed
|
|
258
|
+
* request, the SAME request can succeed on a different account whose tier
|
|
259
|
+
* grants the beta — so this must be treated as retryable on the next account
|
|
260
|
+
* rather than a terminal client-facing 400.
|
|
261
|
+
*/
|
|
262
|
+
export declare function isSubscriptionBetaRejection(status: number, errBody: string): boolean;
|
|
252
263
|
/**
|
|
253
264
|
* Backward-compatible alias — delegates to the shared translation engine.
|
|
254
265
|
*/
|
|
@@ -3131,6 +3131,38 @@ async function handleAnthropicNonOkResponse(args) {
|
|
|
3131
3131
|
durationMs: Date.now() - fetchStartMs,
|
|
3132
3132
|
});
|
|
3133
3133
|
if (isInvalidRequestError(response.status, errBody)) {
|
|
3134
|
+
if (isSubscriptionBetaRejection(response.status, errBody)) {
|
|
3135
|
+
// Subscription-specific beta rejection (an account whose plan tier lacks
|
|
3136
|
+
// an optional beta the proxy injected). The SAME request can succeed on
|
|
3137
|
+
// another account whose tier grants the beta — or on a fallback provider —
|
|
3138
|
+
// so advance to the next account instead of failing the client here.
|
|
3139
|
+
//
|
|
3140
|
+
// Deliberately do NOT store this in `currentInvalidRequestFailure`: that
|
|
3141
|
+
// field is a deterministic "malformed request" signal that both
|
|
3142
|
+
// suppresses provider fallback (shouldAttemptClaudeFallback) and outranks
|
|
3143
|
+
// transient/rate-limit failures in the final response. A beta rejection is
|
|
3144
|
+
// neither terminal nor higher-priority — a later account's real 429/5xx
|
|
3145
|
+
// must still take precedence, and fallback must stay eligible. The reason
|
|
3146
|
+
// is carried in `lastError`, so if every account rejects the beta the
|
|
3147
|
+
// exhaustion response still explains why.
|
|
3148
|
+
logger.always(`[proxy] ← ${response.status} account=${account.label} beta unavailable for subscription; advancing to next account`);
|
|
3149
|
+
logAttempt(response.status, "invalid_request_error", summarizeErrorMessage(errBody));
|
|
3150
|
+
tracer?.setError("invalid_request_error", summarizeErrorMessage(errBody));
|
|
3151
|
+
tracer?.recordRetry(account.label, "beta_unavailable");
|
|
3152
|
+
// A tier's lack of a beta is stable, not transient — advance the
|
|
3153
|
+
// fill-first primary pointer (like the auth/rate-limit rotation paths) so
|
|
3154
|
+
// this account stops being retried first on every future request.
|
|
3155
|
+
advancePrimaryIfCurrent(account.key, enabledAccounts.length, orderedAccounts[0]?.key);
|
|
3156
|
+
currentLastError = summarizeErrorMessage(errBody);
|
|
3157
|
+
return {
|
|
3158
|
+
continueLoop: true,
|
|
3159
|
+
lastError: currentLastError,
|
|
3160
|
+
authFailureMessage: currentAuthFailureMessage,
|
|
3161
|
+
sawTransientFailure: currentSawTransientFailure,
|
|
3162
|
+
invalidRequestFailure: currentInvalidRequestFailure,
|
|
3163
|
+
upstreamSpan: undefined,
|
|
3164
|
+
};
|
|
3165
|
+
}
|
|
3134
3166
|
logger.always(`[proxy] ← ${response.status} upstream invalid_request_error`);
|
|
3135
3167
|
logAttempt(response.status, "invalid_request_error", summarizeErrorMessage(errBody));
|
|
3136
3168
|
tracer?.setError("invalid_request_error", summarizeErrorMessage(errBody));
|
|
@@ -4613,6 +4645,29 @@ export function isInvalidRequestError(status, errBody) {
|
|
|
4613
4645
|
return (parsed.errorType === "invalid_request_error" ||
|
|
4614
4646
|
errBody.includes("invalid_request_error"));
|
|
4615
4647
|
}
|
|
4648
|
+
/**
|
|
4649
|
+
* A subscription-specific beta rejection. Anthropic returns
|
|
4650
|
+
* `400 invalid_request_error` with a message like
|
|
4651
|
+
* "The long context beta is not yet available for this subscription." when an
|
|
4652
|
+
* account's plan tier does not grant an optional beta the proxy injected (see
|
|
4653
|
+
* CLAUDE_CODE_OAUTH_BETAS in anthropicOAuth.ts). Unlike a genuinely malformed
|
|
4654
|
+
* request, the SAME request can succeed on a different account whose tier
|
|
4655
|
+
* grants the beta — so this must be treated as retryable on the next account
|
|
4656
|
+
* rather than a terminal client-facing 400.
|
|
4657
|
+
*/
|
|
4658
|
+
export function isSubscriptionBetaRejection(status, errBody) {
|
|
4659
|
+
if (status !== 400) {
|
|
4660
|
+
return false;
|
|
4661
|
+
}
|
|
4662
|
+
const parsed = parseClaudeErrorBody(errBody);
|
|
4663
|
+
if (parsed.errorType !== "invalid_request_error") {
|
|
4664
|
+
return false;
|
|
4665
|
+
}
|
|
4666
|
+
const message = (parsed.message ?? "").toLowerCase();
|
|
4667
|
+
return (message.includes("beta") &&
|
|
4668
|
+
message.includes("subscription") &&
|
|
4669
|
+
message.includes("available"));
|
|
4670
|
+
}
|
|
4616
4671
|
function normalizeClaudeRequestForAnthropic(body) {
|
|
4617
4672
|
return {
|
|
4618
4673
|
...body,
|
|
@@ -23,6 +23,7 @@ async function getArchiveProcessor() {
|
|
|
23
23
|
import { tracers, ATTR, withSpan } from "../telemetry/index.js";
|
|
24
24
|
import { CSVProcessor } from "./csvProcessor.js";
|
|
25
25
|
import { ImageProcessor } from "./imageProcessor.js";
|
|
26
|
+
import { detectIsoBmffImageMimeType, hasFtypBoxSignature } from "./isoBmff.js";
|
|
26
27
|
import { logger } from "./logger.js";
|
|
27
28
|
import { withTimeout } from "./errorHandling.js";
|
|
28
29
|
import { normalizeUrlForCache, redactUrlForError, sanitizeErrorCause, } from "./logSanitize.js";
|
|
@@ -1659,17 +1660,35 @@ class MagicBytesStrategy {
|
|
|
1659
1660
|
if (this.isWebP(input)) {
|
|
1660
1661
|
return this.result("image", "image/webp", 95);
|
|
1661
1662
|
}
|
|
1663
|
+
if (input.length >= 2 && input[0] === 0x42 && input[1] === 0x4d) {
|
|
1664
|
+
return this.result("image", "image/bmp", 95);
|
|
1665
|
+
}
|
|
1666
|
+
if (input.length >= 4 &&
|
|
1667
|
+
((input[0] === 0x49 &&
|
|
1668
|
+
input[1] === 0x49 &&
|
|
1669
|
+
input[2] === 0x2a &&
|
|
1670
|
+
input[3] === 0x00) ||
|
|
1671
|
+
(input[0] === 0x4d &&
|
|
1672
|
+
input[1] === 0x4d &&
|
|
1673
|
+
input[2] === 0x00 &&
|
|
1674
|
+
input[3] === 0x2a))) {
|
|
1675
|
+
return this.result("image", "image/tiff", 95);
|
|
1676
|
+
}
|
|
1677
|
+
if (input.length >= 4 &&
|
|
1678
|
+
input[0] === 0x00 &&
|
|
1679
|
+
input[1] === 0x00 &&
|
|
1680
|
+
input[2] === 0x01 &&
|
|
1681
|
+
input[3] === 0x00 &&
|
|
1682
|
+
!hasFtypBoxSignature(input)) {
|
|
1683
|
+
return this.result("image", "image/x-icon", 95);
|
|
1684
|
+
}
|
|
1662
1685
|
if (this.isPDF(input)) {
|
|
1663
1686
|
return this.result("pdf", "application/pdf", 95);
|
|
1664
1687
|
}
|
|
1665
1688
|
// ISO-BMFF ("ftyp" at offset 4): MP4 video, QuickTime MOV, or M4A/M4B/M4P
|
|
1666
1689
|
// audio all share this box — disambiguate by the major brand at offset 8-11,
|
|
1667
1690
|
// otherwise an .m4a audio file is misrouted to the video pipeline.
|
|
1668
|
-
if (input
|
|
1669
|
-
input[4] === 0x66 &&
|
|
1670
|
-
input[5] === 0x74 &&
|
|
1671
|
-
input[6] === 0x79 &&
|
|
1672
|
-
input[7] === 0x70) {
|
|
1691
|
+
if (hasFtypBoxSignature(input)) {
|
|
1673
1692
|
const brand = input.length >= 12 ? input.toString("latin1", 8, 12) : "";
|
|
1674
1693
|
// AVIF images share the ISO-BMFF ftyp box with MP4/MOV; the major brand
|
|
1675
1694
|
// ('avif' still, 'avis' sequence, 'avio' intra-only AV1 image/sequence
|
|
@@ -1677,8 +1696,9 @@ class MagicBytesStrategy {
|
|
|
1677
1696
|
// major_brand by real encoders) distinguishes them. Detect before the
|
|
1678
1697
|
// audio/video branches so an AVIF buffer isn't misrouted to the video
|
|
1679
1698
|
// pipeline (#286).
|
|
1680
|
-
|
|
1681
|
-
|
|
1699
|
+
const imageMimeType = detectIsoBmffImageMimeType(input);
|
|
1700
|
+
if (imageMimeType) {
|
|
1701
|
+
return this.result("image", imageMimeType, 95);
|
|
1682
1702
|
}
|
|
1683
1703
|
if (/^(M4A|M4B|M4P|F4A|F4B)/.test(brand)) {
|
|
1684
1704
|
return this.result("audio", "audio/mp4", 95);
|
|
@@ -1775,6 +1795,16 @@ class MagicBytesStrategy {
|
|
|
1775
1795
|
if (input.length >= 2 && input[0] === 0x1f && input[1] === 0x8b) {
|
|
1776
1796
|
return this.result("archive", "application/gzip", 90);
|
|
1777
1797
|
}
|
|
1798
|
+
// 7z: 37 7A BC AF 27 1C
|
|
1799
|
+
if (input.length >= 6 &&
|
|
1800
|
+
input[0] === 0x37 &&
|
|
1801
|
+
input[1] === 0x7a &&
|
|
1802
|
+
input[2] === 0xbc &&
|
|
1803
|
+
input[3] === 0xaf &&
|
|
1804
|
+
input[4] === 0x27 &&
|
|
1805
|
+
input[5] === 0x1c) {
|
|
1806
|
+
return this.result("archive", "application/x-7z-compressed", 95);
|
|
1807
|
+
}
|
|
1778
1808
|
// RAR: "Rar!"
|
|
1779
1809
|
if (input.length >= 4 &&
|
|
1780
1810
|
input[0] === 0x52 &&
|
|
@@ -2006,6 +2036,8 @@ class ExtensionStrategy {
|
|
|
2006
2036
|
// AI providers don't support SVG format, so we process it as sanitized text
|
|
2007
2037
|
svg: "svg",
|
|
2008
2038
|
avif: "image",
|
|
2039
|
+
heic: "image",
|
|
2040
|
+
heif: "image",
|
|
2009
2041
|
pdf: "pdf",
|
|
2010
2042
|
// Video formats
|
|
2011
2043
|
mp4: "video",
|
|
@@ -2179,6 +2211,8 @@ class ExtensionStrategy {
|
|
|
2179
2211
|
tif: "image/tiff",
|
|
2180
2212
|
svg: "image/svg+xml",
|
|
2181
2213
|
avif: "image/avif",
|
|
2214
|
+
heic: "image/heic",
|
|
2215
|
+
heif: "image/heif",
|
|
2182
2216
|
pdf: "application/pdf",
|
|
2183
2217
|
// Video MIME types
|
|
2184
2218
|
mp4: "video/mp4",
|
|
@@ -11,6 +11,7 @@ import { SYSTEM_LIMITS } from "../core/constants.js";
|
|
|
11
11
|
import { SIZE_LIMITS_BYTES } from "../processors/config/sizeLimits.js";
|
|
12
12
|
import { getImageCache } from "./imageCache.js";
|
|
13
13
|
import { ErrorFactory, NeuroLinkError } from "./errorHandling.js";
|
|
14
|
+
import { detectIsoBmffImageMimeType, hasFtypBoxSignature } from "./isoBmff.js";
|
|
14
15
|
/**
|
|
15
16
|
* Minimum bytes required just to detect an image format from magic bytes (#293).
|
|
16
17
|
*/
|
|
@@ -104,7 +105,7 @@ function isRetryableDownloadError(error) {
|
|
|
104
105
|
}
|
|
105
106
|
/**
|
|
106
107
|
* Reject `detectImageType()`'s `application/octet-stream` sentinel — the
|
|
107
|
-
* honest "no known image signature (PNG/JPEG/GIF/WebP/BMP/TIFF/SVG/AVIF)
|
|
108
|
+
* honest "no known image signature (PNG/JPEG/GIF/WebP/BMP/TIFF/ICO/SVG/AVIF/HEIC/HEIF)
|
|
108
109
|
* matched" fallback (#261/#286). Packaging that sentinel into a data URI
|
|
109
110
|
* hands vision providers (OpenAI/Anthropic/Google) a MIME type they reject
|
|
110
111
|
* outright with an HTTP 400, so every public conversion path that turns
|
|
@@ -118,7 +119,7 @@ function assertKnownImageType(mediaType) {
|
|
|
118
119
|
if (mediaType === "application/octet-stream") {
|
|
119
120
|
logger.error("Unable to detect a supported image format from file content");
|
|
120
121
|
throw new Error("Unsupported or corrupted image: no known image signature " +
|
|
121
|
-
"(PNG/JPEG/GIF/WebP/BMP/TIFF/SVG/AVIF) was found in the file content.");
|
|
122
|
+
"(PNG/JPEG/GIF/WebP/BMP/TIFF/ICO/SVG/AVIF/HEIC/HEIF) was found in the file content.");
|
|
122
123
|
}
|
|
123
124
|
}
|
|
124
125
|
/**
|
|
@@ -457,24 +458,22 @@ export class ImageProcessor {
|
|
|
457
458
|
return "image/svg+xml";
|
|
458
459
|
}
|
|
459
460
|
}
|
|
460
|
-
//
|
|
461
|
-
//
|
|
462
|
-
//
|
|
463
|
-
//
|
|
464
|
-
//
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
return "image/avif";
|
|
477
|
-
}
|
|
461
|
+
// ISO-BMFF images: "ftyp" box at offset 4, major brand at offset 8.
|
|
462
|
+
// AVIF has codec-specific major brands. HEIC uses HEVC image/sequence
|
|
463
|
+
// brands, while generic HEIF structural brands (mif1/msf1) need a
|
|
464
|
+
// compatible codec brand to avoid mistaking AVIF or generic MP4 data
|
|
465
|
+
// for HEIF.
|
|
466
|
+
const isoBmffMimeType = detectIsoBmffImageMimeType(input);
|
|
467
|
+
if (isoBmffMimeType) {
|
|
468
|
+
return isoBmffMimeType;
|
|
469
|
+
}
|
|
470
|
+
// ICO: 00 00 01 00 (icon type=1)
|
|
471
|
+
if (input[0] === 0x00 &&
|
|
472
|
+
input[1] === 0x00 &&
|
|
473
|
+
input[2] === 0x01 &&
|
|
474
|
+
input[3] === 0x00 &&
|
|
475
|
+
!hasFtypBoxSignature(input)) {
|
|
476
|
+
return "image/x-icon";
|
|
478
477
|
}
|
|
479
478
|
// BMP: "BM" magic (0x42 0x4D)
|
|
480
479
|
if (input[0] === 0x42 && input[1] === 0x4d) {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared ISO-BMFF (`ftyp`) image-brand detection.
|
|
3
|
+
*
|
|
4
|
+
* FileDetector and ImageProcessor must use the same brand tables and declared
|
|
5
|
+
* box boundary when classifying AVIF, HEIC, and structural HEIF brands. Keeping
|
|
6
|
+
* the scan here prevents the two consumers from drifting as new brands are
|
|
7
|
+
* added.
|
|
8
|
+
*/
|
|
9
|
+
export declare const AVIF_FTYP_BRANDS: Set<string>;
|
|
10
|
+
export declare const HEIC_FTYP_BRANDS: Set<string>;
|
|
11
|
+
export declare const HEIF_STRUCTURAL_FTYP_BRANDS: Set<string>;
|
|
12
|
+
/**
|
|
13
|
+
* Whether the buffer starts with an ISO-BMFF `ftyp` box signature.
|
|
14
|
+
*/
|
|
15
|
+
export declare function hasFtypBoxSignature(input: Buffer): boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Read compatible brands without scanning past the declared `ftyp` box.
|
|
18
|
+
*/
|
|
19
|
+
export declare function getCompatibleFtypBrands(input: Buffer): Set<string>;
|
|
20
|
+
/**
|
|
21
|
+
* Return the image MIME type encoded by an ISO-BMFF `ftyp` box, or null when
|
|
22
|
+
* the box belongs to a non-image ISO-BMFF flavor.
|
|
23
|
+
*/
|
|
24
|
+
export declare function detectIsoBmffImageMimeType(input: Buffer): string | null;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared ISO-BMFF (`ftyp`) image-brand detection.
|
|
3
|
+
*
|
|
4
|
+
* FileDetector and ImageProcessor must use the same brand tables and declared
|
|
5
|
+
* box boundary when classifying AVIF, HEIC, and structural HEIF brands. Keeping
|
|
6
|
+
* the scan here prevents the two consumers from drifting as new brands are
|
|
7
|
+
* added.
|
|
8
|
+
*/
|
|
9
|
+
export const AVIF_FTYP_BRANDS = new Set(["avif", "avis", "avio"]);
|
|
10
|
+
export const HEIC_FTYP_BRANDS = new Set([
|
|
11
|
+
"heic",
|
|
12
|
+
"heix",
|
|
13
|
+
"hevc",
|
|
14
|
+
"hevx",
|
|
15
|
+
"heim",
|
|
16
|
+
"heis",
|
|
17
|
+
"hevm",
|
|
18
|
+
"hevs",
|
|
19
|
+
]);
|
|
20
|
+
export const HEIF_STRUCTURAL_FTYP_BRANDS = new Set(["mif1", "msf1"]);
|
|
21
|
+
/**
|
|
22
|
+
* Whether the buffer starts with an ISO-BMFF `ftyp` box signature.
|
|
23
|
+
*/
|
|
24
|
+
export function hasFtypBoxSignature(input) {
|
|
25
|
+
return (input.length >= 8 &&
|
|
26
|
+
input[4] === 0x66 &&
|
|
27
|
+
input[5] === 0x74 &&
|
|
28
|
+
input[6] === 0x79 &&
|
|
29
|
+
input[7] === 0x70);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Read compatible brands without scanning past the declared `ftyp` box.
|
|
33
|
+
*/
|
|
34
|
+
export function getCompatibleFtypBrands(input) {
|
|
35
|
+
const compatibleBrands = new Set();
|
|
36
|
+
if (input.length < 20) {
|
|
37
|
+
return compatibleBrands;
|
|
38
|
+
}
|
|
39
|
+
const declaredBoxSize = input.readUInt32BE(0);
|
|
40
|
+
const boxEnd = declaredBoxSize === 0
|
|
41
|
+
? input.length
|
|
42
|
+
: Math.min(input.length, declaredBoxSize);
|
|
43
|
+
for (let offset = 16; offset + 4 <= boxEnd; offset += 4) {
|
|
44
|
+
compatibleBrands.add(input.toString("latin1", offset, offset + 4));
|
|
45
|
+
}
|
|
46
|
+
return compatibleBrands;
|
|
47
|
+
}
|
|
48
|
+
function hasFtypBrand(compatibleBrands, candidates) {
|
|
49
|
+
for (const brand of compatibleBrands) {
|
|
50
|
+
if (candidates.has(brand)) {
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Return the image MIME type encoded by an ISO-BMFF `ftyp` box, or null when
|
|
58
|
+
* the box belongs to a non-image ISO-BMFF flavor.
|
|
59
|
+
*/
|
|
60
|
+
export function detectIsoBmffImageMimeType(input) {
|
|
61
|
+
if (input.length < 12 || !hasFtypBoxSignature(input)) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
const brand = input.toString("latin1", 8, 12);
|
|
65
|
+
if (AVIF_FTYP_BRANDS.has(brand)) {
|
|
66
|
+
return "image/avif";
|
|
67
|
+
}
|
|
68
|
+
if (HEIC_FTYP_BRANDS.has(brand)) {
|
|
69
|
+
return "image/heic";
|
|
70
|
+
}
|
|
71
|
+
if (!HEIF_STRUCTURAL_FTYP_BRANDS.has(brand)) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
const compatibleBrands = getCompatibleFtypBrands(input);
|
|
75
|
+
if (hasFtypBrand(compatibleBrands, HEIC_FTYP_BRANDS)) {
|
|
76
|
+
return "image/heif";
|
|
77
|
+
}
|
|
78
|
+
if (hasFtypBrand(compatibleBrands, AVIF_FTYP_BRANDS)) {
|
|
79
|
+
return "image/avif";
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=isoBmff.js.map
|
|
@@ -283,6 +283,8 @@ export const EXTENSION_MIME_MAP = {
|
|
|
283
283
|
".webp": IMAGE_MIME_TYPES.WEBP,
|
|
284
284
|
".bmp": IMAGE_MIME_TYPES.BMP,
|
|
285
285
|
".ico": IMAGE_MIME_TYPES.ICO,
|
|
286
|
+
".heic": IMAGE_MIME_TYPES.HEIC,
|
|
287
|
+
".heif": IMAGE_MIME_TYPES.HEIF,
|
|
286
288
|
// Source code
|
|
287
289
|
".js": TEXT_MIME_TYPES.JAVASCRIPT,
|
|
288
290
|
".mjs": TEXT_MIME_TYPES.JAVASCRIPT,
|
|
@@ -249,6 +249,17 @@ export declare function parseClaudeErrorBody(errBody: string): ParsedClaudeError
|
|
|
249
249
|
* Detect malformed request errors that should not trigger account/provider failover.
|
|
250
250
|
*/
|
|
251
251
|
export declare function isInvalidRequestError(status: number, errBody: string): boolean;
|
|
252
|
+
/**
|
|
253
|
+
* A subscription-specific beta rejection. Anthropic returns
|
|
254
|
+
* `400 invalid_request_error` with a message like
|
|
255
|
+
* "The long context beta is not yet available for this subscription." when an
|
|
256
|
+
* account's plan tier does not grant an optional beta the proxy injected (see
|
|
257
|
+
* CLAUDE_CODE_OAUTH_BETAS in anthropicOAuth.ts). Unlike a genuinely malformed
|
|
258
|
+
* request, the SAME request can succeed on a different account whose tier
|
|
259
|
+
* grants the beta — so this must be treated as retryable on the next account
|
|
260
|
+
* rather than a terminal client-facing 400.
|
|
261
|
+
*/
|
|
262
|
+
export declare function isSubscriptionBetaRejection(status: number, errBody: string): boolean;
|
|
252
263
|
/**
|
|
253
264
|
* Backward-compatible alias — delegates to the shared translation engine.
|
|
254
265
|
*/
|
|
@@ -3131,6 +3131,38 @@ async function handleAnthropicNonOkResponse(args) {
|
|
|
3131
3131
|
durationMs: Date.now() - fetchStartMs,
|
|
3132
3132
|
});
|
|
3133
3133
|
if (isInvalidRequestError(response.status, errBody)) {
|
|
3134
|
+
if (isSubscriptionBetaRejection(response.status, errBody)) {
|
|
3135
|
+
// Subscription-specific beta rejection (an account whose plan tier lacks
|
|
3136
|
+
// an optional beta the proxy injected). The SAME request can succeed on
|
|
3137
|
+
// another account whose tier grants the beta — or on a fallback provider —
|
|
3138
|
+
// so advance to the next account instead of failing the client here.
|
|
3139
|
+
//
|
|
3140
|
+
// Deliberately do NOT store this in `currentInvalidRequestFailure`: that
|
|
3141
|
+
// field is a deterministic "malformed request" signal that both
|
|
3142
|
+
// suppresses provider fallback (shouldAttemptClaudeFallback) and outranks
|
|
3143
|
+
// transient/rate-limit failures in the final response. A beta rejection is
|
|
3144
|
+
// neither terminal nor higher-priority — a later account's real 429/5xx
|
|
3145
|
+
// must still take precedence, and fallback must stay eligible. The reason
|
|
3146
|
+
// is carried in `lastError`, so if every account rejects the beta the
|
|
3147
|
+
// exhaustion response still explains why.
|
|
3148
|
+
logger.always(`[proxy] ← ${response.status} account=${account.label} beta unavailable for subscription; advancing to next account`);
|
|
3149
|
+
logAttempt(response.status, "invalid_request_error", summarizeErrorMessage(errBody));
|
|
3150
|
+
tracer?.setError("invalid_request_error", summarizeErrorMessage(errBody));
|
|
3151
|
+
tracer?.recordRetry(account.label, "beta_unavailable");
|
|
3152
|
+
// A tier's lack of a beta is stable, not transient — advance the
|
|
3153
|
+
// fill-first primary pointer (like the auth/rate-limit rotation paths) so
|
|
3154
|
+
// this account stops being retried first on every future request.
|
|
3155
|
+
advancePrimaryIfCurrent(account.key, enabledAccounts.length, orderedAccounts[0]?.key);
|
|
3156
|
+
currentLastError = summarizeErrorMessage(errBody);
|
|
3157
|
+
return {
|
|
3158
|
+
continueLoop: true,
|
|
3159
|
+
lastError: currentLastError,
|
|
3160
|
+
authFailureMessage: currentAuthFailureMessage,
|
|
3161
|
+
sawTransientFailure: currentSawTransientFailure,
|
|
3162
|
+
invalidRequestFailure: currentInvalidRequestFailure,
|
|
3163
|
+
upstreamSpan: undefined,
|
|
3164
|
+
};
|
|
3165
|
+
}
|
|
3134
3166
|
logger.always(`[proxy] ← ${response.status} upstream invalid_request_error`);
|
|
3135
3167
|
logAttempt(response.status, "invalid_request_error", summarizeErrorMessage(errBody));
|
|
3136
3168
|
tracer?.setError("invalid_request_error", summarizeErrorMessage(errBody));
|
|
@@ -4613,6 +4645,29 @@ export function isInvalidRequestError(status, errBody) {
|
|
|
4613
4645
|
return (parsed.errorType === "invalid_request_error" ||
|
|
4614
4646
|
errBody.includes("invalid_request_error"));
|
|
4615
4647
|
}
|
|
4648
|
+
/**
|
|
4649
|
+
* A subscription-specific beta rejection. Anthropic returns
|
|
4650
|
+
* `400 invalid_request_error` with a message like
|
|
4651
|
+
* "The long context beta is not yet available for this subscription." when an
|
|
4652
|
+
* account's plan tier does not grant an optional beta the proxy injected (see
|
|
4653
|
+
* CLAUDE_CODE_OAUTH_BETAS in anthropicOAuth.ts). Unlike a genuinely malformed
|
|
4654
|
+
* request, the SAME request can succeed on a different account whose tier
|
|
4655
|
+
* grants the beta — so this must be treated as retryable on the next account
|
|
4656
|
+
* rather than a terminal client-facing 400.
|
|
4657
|
+
*/
|
|
4658
|
+
export function isSubscriptionBetaRejection(status, errBody) {
|
|
4659
|
+
if (status !== 400) {
|
|
4660
|
+
return false;
|
|
4661
|
+
}
|
|
4662
|
+
const parsed = parseClaudeErrorBody(errBody);
|
|
4663
|
+
if (parsed.errorType !== "invalid_request_error") {
|
|
4664
|
+
return false;
|
|
4665
|
+
}
|
|
4666
|
+
const message = (parsed.message ?? "").toLowerCase();
|
|
4667
|
+
return (message.includes("beta") &&
|
|
4668
|
+
message.includes("subscription") &&
|
|
4669
|
+
message.includes("available"));
|
|
4670
|
+
}
|
|
4616
4671
|
function normalizeClaudeRequestForAnthropic(body) {
|
|
4617
4672
|
return {
|
|
4618
4673
|
...body,
|
|
@@ -23,6 +23,7 @@ async function getArchiveProcessor() {
|
|
|
23
23
|
import { tracers, ATTR, withSpan } from "../telemetry/index.js";
|
|
24
24
|
import { CSVProcessor } from "./csvProcessor.js";
|
|
25
25
|
import { ImageProcessor } from "./imageProcessor.js";
|
|
26
|
+
import { detectIsoBmffImageMimeType, hasFtypBoxSignature } from "./isoBmff.js";
|
|
26
27
|
import { logger } from "./logger.js";
|
|
27
28
|
import { withTimeout } from "./errorHandling.js";
|
|
28
29
|
import { normalizeUrlForCache, redactUrlForError, sanitizeErrorCause, } from "./logSanitize.js";
|
|
@@ -1659,17 +1660,35 @@ class MagicBytesStrategy {
|
|
|
1659
1660
|
if (this.isWebP(input)) {
|
|
1660
1661
|
return this.result("image", "image/webp", 95);
|
|
1661
1662
|
}
|
|
1663
|
+
if (input.length >= 2 && input[0] === 0x42 && input[1] === 0x4d) {
|
|
1664
|
+
return this.result("image", "image/bmp", 95);
|
|
1665
|
+
}
|
|
1666
|
+
if (input.length >= 4 &&
|
|
1667
|
+
((input[0] === 0x49 &&
|
|
1668
|
+
input[1] === 0x49 &&
|
|
1669
|
+
input[2] === 0x2a &&
|
|
1670
|
+
input[3] === 0x00) ||
|
|
1671
|
+
(input[0] === 0x4d &&
|
|
1672
|
+
input[1] === 0x4d &&
|
|
1673
|
+
input[2] === 0x00 &&
|
|
1674
|
+
input[3] === 0x2a))) {
|
|
1675
|
+
return this.result("image", "image/tiff", 95);
|
|
1676
|
+
}
|
|
1677
|
+
if (input.length >= 4 &&
|
|
1678
|
+
input[0] === 0x00 &&
|
|
1679
|
+
input[1] === 0x00 &&
|
|
1680
|
+
input[2] === 0x01 &&
|
|
1681
|
+
input[3] === 0x00 &&
|
|
1682
|
+
!hasFtypBoxSignature(input)) {
|
|
1683
|
+
return this.result("image", "image/x-icon", 95);
|
|
1684
|
+
}
|
|
1662
1685
|
if (this.isPDF(input)) {
|
|
1663
1686
|
return this.result("pdf", "application/pdf", 95);
|
|
1664
1687
|
}
|
|
1665
1688
|
// ISO-BMFF ("ftyp" at offset 4): MP4 video, QuickTime MOV, or M4A/M4B/M4P
|
|
1666
1689
|
// audio all share this box — disambiguate by the major brand at offset 8-11,
|
|
1667
1690
|
// otherwise an .m4a audio file is misrouted to the video pipeline.
|
|
1668
|
-
if (input
|
|
1669
|
-
input[4] === 0x66 &&
|
|
1670
|
-
input[5] === 0x74 &&
|
|
1671
|
-
input[6] === 0x79 &&
|
|
1672
|
-
input[7] === 0x70) {
|
|
1691
|
+
if (hasFtypBoxSignature(input)) {
|
|
1673
1692
|
const brand = input.length >= 12 ? input.toString("latin1", 8, 12) : "";
|
|
1674
1693
|
// AVIF images share the ISO-BMFF ftyp box with MP4/MOV; the major brand
|
|
1675
1694
|
// ('avif' still, 'avis' sequence, 'avio' intra-only AV1 image/sequence
|
|
@@ -1677,8 +1696,9 @@ class MagicBytesStrategy {
|
|
|
1677
1696
|
// major_brand by real encoders) distinguishes them. Detect before the
|
|
1678
1697
|
// audio/video branches so an AVIF buffer isn't misrouted to the video
|
|
1679
1698
|
// pipeline (#286).
|
|
1680
|
-
|
|
1681
|
-
|
|
1699
|
+
const imageMimeType = detectIsoBmffImageMimeType(input);
|
|
1700
|
+
if (imageMimeType) {
|
|
1701
|
+
return this.result("image", imageMimeType, 95);
|
|
1682
1702
|
}
|
|
1683
1703
|
if (/^(M4A|M4B|M4P|F4A|F4B)/.test(brand)) {
|
|
1684
1704
|
return this.result("audio", "audio/mp4", 95);
|
|
@@ -1775,6 +1795,16 @@ class MagicBytesStrategy {
|
|
|
1775
1795
|
if (input.length >= 2 && input[0] === 0x1f && input[1] === 0x8b) {
|
|
1776
1796
|
return this.result("archive", "application/gzip", 90);
|
|
1777
1797
|
}
|
|
1798
|
+
// 7z: 37 7A BC AF 27 1C
|
|
1799
|
+
if (input.length >= 6 &&
|
|
1800
|
+
input[0] === 0x37 &&
|
|
1801
|
+
input[1] === 0x7a &&
|
|
1802
|
+
input[2] === 0xbc &&
|
|
1803
|
+
input[3] === 0xaf &&
|
|
1804
|
+
input[4] === 0x27 &&
|
|
1805
|
+
input[5] === 0x1c) {
|
|
1806
|
+
return this.result("archive", "application/x-7z-compressed", 95);
|
|
1807
|
+
}
|
|
1778
1808
|
// RAR: "Rar!"
|
|
1779
1809
|
if (input.length >= 4 &&
|
|
1780
1810
|
input[0] === 0x52 &&
|
|
@@ -2006,6 +2036,8 @@ class ExtensionStrategy {
|
|
|
2006
2036
|
// AI providers don't support SVG format, so we process it as sanitized text
|
|
2007
2037
|
svg: "svg",
|
|
2008
2038
|
avif: "image",
|
|
2039
|
+
heic: "image",
|
|
2040
|
+
heif: "image",
|
|
2009
2041
|
pdf: "pdf",
|
|
2010
2042
|
// Video formats
|
|
2011
2043
|
mp4: "video",
|
|
@@ -2179,6 +2211,8 @@ class ExtensionStrategy {
|
|
|
2179
2211
|
tif: "image/tiff",
|
|
2180
2212
|
svg: "image/svg+xml",
|
|
2181
2213
|
avif: "image/avif",
|
|
2214
|
+
heic: "image/heic",
|
|
2215
|
+
heif: "image/heif",
|
|
2182
2216
|
pdf: "application/pdf",
|
|
2183
2217
|
// Video MIME types
|
|
2184
2218
|
mp4: "video/mp4",
|
|
@@ -11,6 +11,7 @@ import { SYSTEM_LIMITS } from "../core/constants.js";
|
|
|
11
11
|
import { SIZE_LIMITS_BYTES } from "../processors/config/sizeLimits.js";
|
|
12
12
|
import { getImageCache } from "./imageCache.js";
|
|
13
13
|
import { ErrorFactory, NeuroLinkError } from "./errorHandling.js";
|
|
14
|
+
import { detectIsoBmffImageMimeType, hasFtypBoxSignature } from "./isoBmff.js";
|
|
14
15
|
/**
|
|
15
16
|
* Minimum bytes required just to detect an image format from magic bytes (#293).
|
|
16
17
|
*/
|
|
@@ -104,7 +105,7 @@ function isRetryableDownloadError(error) {
|
|
|
104
105
|
}
|
|
105
106
|
/**
|
|
106
107
|
* Reject `detectImageType()`'s `application/octet-stream` sentinel — the
|
|
107
|
-
* honest "no known image signature (PNG/JPEG/GIF/WebP/BMP/TIFF/SVG/AVIF)
|
|
108
|
+
* honest "no known image signature (PNG/JPEG/GIF/WebP/BMP/TIFF/ICO/SVG/AVIF/HEIC/HEIF)
|
|
108
109
|
* matched" fallback (#261/#286). Packaging that sentinel into a data URI
|
|
109
110
|
* hands vision providers (OpenAI/Anthropic/Google) a MIME type they reject
|
|
110
111
|
* outright with an HTTP 400, so every public conversion path that turns
|
|
@@ -118,7 +119,7 @@ function assertKnownImageType(mediaType) {
|
|
|
118
119
|
if (mediaType === "application/octet-stream") {
|
|
119
120
|
logger.error("Unable to detect a supported image format from file content");
|
|
120
121
|
throw new Error("Unsupported or corrupted image: no known image signature " +
|
|
121
|
-
"(PNG/JPEG/GIF/WebP/BMP/TIFF/SVG/AVIF) was found in the file content.");
|
|
122
|
+
"(PNG/JPEG/GIF/WebP/BMP/TIFF/ICO/SVG/AVIF/HEIC/HEIF) was found in the file content.");
|
|
122
123
|
}
|
|
123
124
|
}
|
|
124
125
|
/**
|
|
@@ -457,24 +458,22 @@ export class ImageProcessor {
|
|
|
457
458
|
return "image/svg+xml";
|
|
458
459
|
}
|
|
459
460
|
}
|
|
460
|
-
//
|
|
461
|
-
//
|
|
462
|
-
//
|
|
463
|
-
//
|
|
464
|
-
//
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
return "image/avif";
|
|
477
|
-
}
|
|
461
|
+
// ISO-BMFF images: "ftyp" box at offset 4, major brand at offset 8.
|
|
462
|
+
// AVIF has codec-specific major brands. HEIC uses HEVC image/sequence
|
|
463
|
+
// brands, while generic HEIF structural brands (mif1/msf1) need a
|
|
464
|
+
// compatible codec brand to avoid mistaking AVIF or generic MP4 data
|
|
465
|
+
// for HEIF.
|
|
466
|
+
const isoBmffMimeType = detectIsoBmffImageMimeType(input);
|
|
467
|
+
if (isoBmffMimeType) {
|
|
468
|
+
return isoBmffMimeType;
|
|
469
|
+
}
|
|
470
|
+
// ICO: 00 00 01 00 (icon type=1)
|
|
471
|
+
if (input[0] === 0x00 &&
|
|
472
|
+
input[1] === 0x00 &&
|
|
473
|
+
input[2] === 0x01 &&
|
|
474
|
+
input[3] === 0x00 &&
|
|
475
|
+
!hasFtypBoxSignature(input)) {
|
|
476
|
+
return "image/x-icon";
|
|
478
477
|
}
|
|
479
478
|
// BMP: "BM" magic (0x42 0x4D)
|
|
480
479
|
if (input[0] === 0x42 && input[1] === 0x4d) {
|