@juspay/neurolink 10.4.3 → 10.5.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/CHANGELOG.md +12 -0
- package/dist/auth/tokenStore.d.ts +7 -0
- package/dist/auth/tokenStore.js +54 -0
- package/dist/browser/neurolink.min.js +374 -374
- package/dist/cli/commands/proxy.d.ts +1 -0
- package/dist/cli/commands/proxy.js +183 -24
- package/dist/lib/auth/tokenStore.d.ts +7 -0
- package/dist/lib/auth/tokenStore.js +54 -0
- package/dist/lib/processors/config/mimeConstants.js +2 -0
- package/dist/lib/proxy/proxyTranslationEngine.d.ts +1 -0
- package/dist/lib/proxy/proxyTranslationEngine.js +56 -12
- package/dist/lib/proxy/rawStreamCapture.js +47 -1
- package/dist/lib/proxy/sseInterceptor.js +47 -1
- package/dist/lib/proxy/tokenRefresh.d.ts +6 -0
- package/dist/lib/proxy/tokenRefresh.js +70 -15
- package/dist/lib/proxy/usageStats.d.ts +25 -3
- package/dist/lib/proxy/usageStats.js +546 -55
- package/dist/lib/server/routes/claudeProxyRoutes.d.ts +2 -0
- package/dist/lib/server/routes/claudeProxyRoutes.js +198 -222
- package/dist/lib/types/proxy.d.ts +60 -1
- 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/proxy/proxyTranslationEngine.d.ts +1 -0
- package/dist/proxy/proxyTranslationEngine.js +56 -12
- package/dist/proxy/rawStreamCapture.js +47 -1
- package/dist/proxy/sseInterceptor.js +47 -1
- package/dist/proxy/tokenRefresh.d.ts +6 -0
- package/dist/proxy/tokenRefresh.js +70 -15
- package/dist/proxy/usageStats.d.ts +25 -3
- package/dist/proxy/usageStats.js +546 -55
- package/dist/server/routes/claudeProxyRoutes.d.ts +2 -0
- package/dist/server/routes/claudeProxyRoutes.js +198 -222
- package/dist/types/proxy.d.ts +60 -1
- 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
|
@@ -671,6 +671,29 @@ export type AnthropicUpstreamFetchResult = {
|
|
|
671
671
|
sawNetworkError: boolean;
|
|
672
672
|
upstreamSpan?: Span;
|
|
673
673
|
};
|
|
674
|
+
export type ProxyTerminalErrorCategory = "authentication" | "client_cancelled" | "fallback_exhausted" | "invalid_request" | "proxy_error" | "rate_limit" | "stream_error" | "unclassified" | "upstream_error" | "other";
|
|
675
|
+
/** Compact, redacted terminal failure retained independently of body logs. */
|
|
676
|
+
export type ProxyTerminalErrorSummary = {
|
|
677
|
+
id: string;
|
|
678
|
+
at: number;
|
|
679
|
+
status: number;
|
|
680
|
+
category: ProxyTerminalErrorCategory;
|
|
681
|
+
requestId?: string;
|
|
682
|
+
account?: string;
|
|
683
|
+
accountType?: string;
|
|
684
|
+
errorType?: string;
|
|
685
|
+
errorCode?: string;
|
|
686
|
+
terminalOutcome?: string;
|
|
687
|
+
message?: string;
|
|
688
|
+
};
|
|
689
|
+
/** Optional terminal context supplied when a final request error is recorded. */
|
|
690
|
+
export type ProxyTerminalErrorDetails = {
|
|
691
|
+
requestId?: string;
|
|
692
|
+
errorType?: string;
|
|
693
|
+
errorCode?: string;
|
|
694
|
+
terminalOutcome?: string;
|
|
695
|
+
message?: string;
|
|
696
|
+
};
|
|
674
697
|
export type AccountStats = {
|
|
675
698
|
label: string;
|
|
676
699
|
type: string;
|
|
@@ -700,6 +723,19 @@ export type ProxyStats = {
|
|
|
700
723
|
totalQuotaRateLimits: number;
|
|
701
724
|
accounts: Record<string, AccountStats>;
|
|
702
725
|
};
|
|
726
|
+
/** Bounded terminal-error state stored separately from counters and body logs. */
|
|
727
|
+
export type ProxyTerminalErrorJournal = {
|
|
728
|
+
startedAt: number;
|
|
729
|
+
totalErrors: number;
|
|
730
|
+
counts: Record<ProxyTerminalErrorCategory, number>;
|
|
731
|
+
recent: ProxyTerminalErrorSummary[];
|
|
732
|
+
};
|
|
733
|
+
export type ProxyUsageStatsSnapshot = {
|
|
734
|
+
stats: ProxyStats;
|
|
735
|
+
statsVersion: number;
|
|
736
|
+
terminalErrors: ProxyTerminalErrorJournal;
|
|
737
|
+
terminalErrorsVersion: number;
|
|
738
|
+
};
|
|
703
739
|
/** Durability and reconciliation state for the proxy usage counters. */
|
|
704
740
|
export type ProxyStatsPersistenceStatus = {
|
|
705
741
|
enabled: boolean;
|
|
@@ -712,6 +748,14 @@ export type ProxyStatsPersistenceStatus = {
|
|
|
712
748
|
lastReconciledAt: number | null;
|
|
713
749
|
lastRecoveryAt: number | null;
|
|
714
750
|
lastError: string | null;
|
|
751
|
+
terminalErrorsFilePath?: string | null;
|
|
752
|
+
terminalErrorsRevision?: number;
|
|
753
|
+
terminalErrorsPending?: number;
|
|
754
|
+
terminalErrorsInFlight?: number;
|
|
755
|
+
terminalErrorsUnpersisted?: number;
|
|
756
|
+
terminalErrorsLastFlushedAt?: number | null;
|
|
757
|
+
terminalErrorsLastRecoveryAt?: number | null;
|
|
758
|
+
terminalErrorsLastError?: string | null;
|
|
715
759
|
};
|
|
716
760
|
/** Versioned on-disk snapshot shared by overlapping proxy workers. */
|
|
717
761
|
export type PersistedProxyStatsSnapshot = {
|
|
@@ -720,6 +764,13 @@ export type PersistedProxyStatsSnapshot = {
|
|
|
720
764
|
updatedAt: number;
|
|
721
765
|
stats: ProxyStats;
|
|
722
766
|
};
|
|
767
|
+
/** Versioned terminal-error snapshot isolated from rolling-worker counter writes. */
|
|
768
|
+
export type PersistedProxyTerminalErrorSnapshot = {
|
|
769
|
+
schemaVersion: 1;
|
|
770
|
+
revision: number;
|
|
771
|
+
updatedAt: number;
|
|
772
|
+
journal: ProxyTerminalErrorJournal;
|
|
773
|
+
};
|
|
723
774
|
/** Ownership metadata for the proxy statistics cross-process lock. */
|
|
724
775
|
export type ProxyStatsLockOwner = {
|
|
725
776
|
token: string;
|
|
@@ -729,6 +780,7 @@ export type ProxyStatsLockOwner = {
|
|
|
729
780
|
/** Construction options for an isolated proxy statistics store. */
|
|
730
781
|
export type ProxyUsageStatsStoreOptions = {
|
|
731
782
|
filePath?: string;
|
|
783
|
+
terminalErrorsFilePath?: string;
|
|
732
784
|
flushIntervalMs?: number;
|
|
733
785
|
lockTimeoutMs?: number;
|
|
734
786
|
staleLockMs?: number;
|
|
@@ -1951,6 +2003,11 @@ export type StatusStats = {
|
|
|
1951
2003
|
totalRateLimits: number;
|
|
1952
2004
|
totalTransientRateLimits?: number;
|
|
1953
2005
|
totalQuotaRateLimits?: number;
|
|
2006
|
+
terminalErrors?: ProxyTerminalErrorJournal;
|
|
2007
|
+
lastTerminalError?: ProxyTerminalErrorSummary | null;
|
|
2008
|
+
terminalErrorDetailsComparable?: boolean;
|
|
2009
|
+
terminalErrorDetailsMissing?: number;
|
|
2010
|
+
terminalErrorDetailsExcess?: number;
|
|
1954
2011
|
accounts?: {
|
|
1955
2012
|
label: string;
|
|
1956
2013
|
type: string;
|
|
@@ -1963,7 +2020,9 @@ export type StatusStats = {
|
|
|
1963
2020
|
transientRateLimits?: number;
|
|
1964
2021
|
quotaRateLimits?: number;
|
|
1965
2022
|
cooling: boolean;
|
|
1966
|
-
|
|
2023
|
+
allowed?: boolean;
|
|
2024
|
+
expired?: boolean;
|
|
2025
|
+
status?: "active" | "cooling" | "disabled" | "expired" | "excluded" | "removed" | "internal" | "unattributed";
|
|
1967
2026
|
}[];
|
|
1968
2027
|
persistence?: ProxyStatsPersistenceStatus;
|
|
1969
2028
|
};
|
|
@@ -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,
|
|
@@ -77,6 +77,7 @@ export declare function handleTranslatedJsonRequest(args: {
|
|
|
77
77
|
attempts: ProxyTranslationAttempt[];
|
|
78
78
|
tracer?: ProxyTracer;
|
|
79
79
|
requestStartTime: number;
|
|
80
|
+
terminalFailureStatus?: number;
|
|
80
81
|
}): Promise<unknown>;
|
|
81
82
|
/**
|
|
82
83
|
* Build the /v1/models response in OpenAI list format.
|
|
@@ -247,6 +247,7 @@ export async function handleTranslatedStreamRequest(args) {
|
|
|
247
247
|
let keepAliveTimer;
|
|
248
248
|
let cancelled = false;
|
|
249
249
|
let succeeded = false;
|
|
250
|
+
let streamInterruptedAfterOutput = false;
|
|
250
251
|
let translatedModel;
|
|
251
252
|
let finalStreamError = "No translation providers succeeded";
|
|
252
253
|
let upstreamIterator;
|
|
@@ -267,6 +268,9 @@ export async function handleTranslatedStreamRequest(args) {
|
|
|
267
268
|
}, KEEPALIVE_INTERVAL_MS);
|
|
268
269
|
try {
|
|
269
270
|
for (let attemptIndex = 0; attemptIndex < attempts.length; attemptIndex++) {
|
|
271
|
+
if (cancelled) {
|
|
272
|
+
break;
|
|
273
|
+
}
|
|
270
274
|
const attempt = attempts[attemptIndex];
|
|
271
275
|
lastAttemptLabel = attempt.label ?? "translation";
|
|
272
276
|
logger.always(`[proxy:${format}] attempt ${attemptIndex + 1}/${attempts.length}: ${attempt.label}`);
|
|
@@ -295,10 +299,14 @@ export async function handleTranslatedStreamRequest(args) {
|
|
|
295
299
|
}
|
|
296
300
|
}
|
|
297
301
|
}
|
|
302
|
+
if (cancelled) {
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
298
305
|
const toolCalls = streamResult.toolCalls ?? [];
|
|
299
306
|
if (!hasTranslatedOutput(collectedText, toolCalls)) {
|
|
300
307
|
finalStreamError = `Translated provider ${attempt.label} returned no content or tool calls`;
|
|
301
308
|
logger.debug(`${tag} translation attempt ${attempt.label} returned no content or tool calls`);
|
|
309
|
+
recordAttemptError(lastAttemptLabel, "translation", 502);
|
|
302
310
|
continue;
|
|
303
311
|
}
|
|
304
312
|
if (!cancelled && toolCalls.length) {
|
|
@@ -331,7 +339,6 @@ export async function handleTranslatedStreamRequest(args) {
|
|
|
331
339
|
tracer?.recordMetrics();
|
|
332
340
|
translatedModel = streamResult.model;
|
|
333
341
|
succeeded = true;
|
|
334
|
-
recordFinalSuccess(lastAttemptLabel, "translation");
|
|
335
342
|
return;
|
|
336
343
|
}
|
|
337
344
|
catch (streamErr) {
|
|
@@ -343,6 +350,8 @@ export async function handleTranslatedStreamRequest(args) {
|
|
|
343
350
|
? streamErr.message
|
|
344
351
|
: String(streamErr);
|
|
345
352
|
if (collectedText.trim().length > 0) {
|
|
353
|
+
streamInterruptedAfterOutput = true;
|
|
354
|
+
recordAttemptError(lastAttemptLabel, "translation", 502);
|
|
346
355
|
logger.always(`${tag} mid-stream error: ${finalStreamError}`);
|
|
347
356
|
for (const frame of serializer.emitError(`Upstream stream interrupted: ${finalStreamError}`)) {
|
|
348
357
|
controller.enqueue(encoder.encode(frame));
|
|
@@ -354,7 +363,6 @@ export async function handleTranslatedStreamRequest(args) {
|
|
|
354
363
|
}
|
|
355
364
|
}
|
|
356
365
|
// All attempts exhausted
|
|
357
|
-
recordFinalError(500, lastAttemptLabel, "translation");
|
|
358
366
|
if (!cancelled) {
|
|
359
367
|
logger.always(`${tag} all translation attempts failed: ${finalStreamError}`);
|
|
360
368
|
for (const frame of serializer.emitError(finalStreamError)) {
|
|
@@ -372,12 +380,38 @@ export async function handleTranslatedStreamRequest(args) {
|
|
|
372
380
|
if (tracer && translatedModel && translatedModel !== requestModel) {
|
|
373
381
|
tracer.setModelSubstitution(requestModel, translatedModel);
|
|
374
382
|
}
|
|
375
|
-
|
|
376
|
-
|
|
383
|
+
const terminalStatus = cancelled
|
|
384
|
+
? 499
|
|
385
|
+
: succeeded
|
|
386
|
+
? 200
|
|
387
|
+
: streamInterruptedAfterOutput
|
|
388
|
+
? 502
|
|
389
|
+
: 500;
|
|
390
|
+
const terminalErrorType = cancelled
|
|
391
|
+
? "client_cancelled"
|
|
392
|
+
: streamInterruptedAfterOutput
|
|
393
|
+
? "stream_error"
|
|
394
|
+
: succeeded
|
|
395
|
+
? undefined
|
|
396
|
+
: "generation_error";
|
|
397
|
+
const terminalErrorMessage = cancelled
|
|
398
|
+
? "Client cancelled the streaming response"
|
|
399
|
+
: succeeded
|
|
400
|
+
? undefined
|
|
401
|
+
: finalStreamError;
|
|
402
|
+
if (terminalErrorType && terminalErrorMessage) {
|
|
403
|
+
tracer?.setError(terminalErrorType, terminalErrorMessage.slice(0, 500));
|
|
404
|
+
recordFinalError(terminalStatus, lastAttemptLabel, "translation", {
|
|
405
|
+
requestId: ctx.requestId,
|
|
406
|
+
errorType: terminalErrorType,
|
|
407
|
+
terminalOutcome: terminalErrorType,
|
|
408
|
+
message: terminalErrorMessage,
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
else {
|
|
412
|
+
recordFinalSuccess(lastAttemptLabel, "translation");
|
|
377
413
|
}
|
|
378
|
-
|
|
379
|
-
// responseStatus below (success path is 200, exhausted-attempts path is 500).
|
|
380
|
-
tracer?.end(succeeded ? 200 : 500, Date.now() - requestStartTime);
|
|
414
|
+
tracer?.end(terminalStatus, Date.now() - requestStartTime);
|
|
381
415
|
const traceCtx = tracer?.getTraceContext();
|
|
382
416
|
logRequest({
|
|
383
417
|
timestamp: new Date().toISOString(),
|
|
@@ -389,8 +423,12 @@ export async function handleTranslatedStreamRequest(args) {
|
|
|
389
423
|
toolCount: Object.keys(parsed.tools).length,
|
|
390
424
|
account: "translation",
|
|
391
425
|
accountType: "translation",
|
|
392
|
-
responseStatus:
|
|
426
|
+
responseStatus: terminalStatus,
|
|
393
427
|
responseTimeMs: Date.now() - requestStartTime,
|
|
428
|
+
...(terminalErrorType ? { errorType: terminalErrorType } : {}),
|
|
429
|
+
...(terminalErrorMessage
|
|
430
|
+
? { errorMessage: terminalErrorMessage.slice(0, 500) }
|
|
431
|
+
: {}),
|
|
394
432
|
...(traceCtx?.traceId ? { traceId: traceCtx.traceId } : {}),
|
|
395
433
|
...(traceCtx?.spanId ? { spanId: traceCtx.spanId } : {}),
|
|
396
434
|
});
|
|
@@ -424,7 +462,7 @@ export async function handleTranslatedStreamRequest(args) {
|
|
|
424
462
|
* Handles a translated non-streaming request for either Claude or OpenAI format.
|
|
425
463
|
*/
|
|
426
464
|
export async function handleTranslatedJsonRequest(args) {
|
|
427
|
-
const { ctx, format, requestModel, parsed, attempts, tracer, requestStartTime, } = args;
|
|
465
|
+
const { ctx, format, requestModel, parsed, attempts, tracer, requestStartTime, terminalFailureStatus = 500, } = args;
|
|
428
466
|
const tag = logTag(format);
|
|
429
467
|
let lastAttemptError = "No translation providers succeeded";
|
|
430
468
|
let lastAttemptLabel = "translation";
|
|
@@ -448,6 +486,7 @@ export async function handleTranslatedJsonRequest(args) {
|
|
|
448
486
|
if (!hasTranslatedOutput(collectedText, streamResult.toolCalls)) {
|
|
449
487
|
lastAttemptError = `Translated provider ${attempt.label} returned no content or tool calls`;
|
|
450
488
|
logger.debug(`${tag} translation attempt ${attempt.label} returned no content or tool calls`);
|
|
489
|
+
recordAttemptError(lastAttemptLabel, "translation", 502);
|
|
451
490
|
continue;
|
|
452
491
|
}
|
|
453
492
|
const internal = {
|
|
@@ -505,9 +544,14 @@ export async function handleTranslatedJsonRequest(args) {
|
|
|
505
544
|
recordAttemptError(lastAttemptLabel, "translation", 500);
|
|
506
545
|
}
|
|
507
546
|
}
|
|
508
|
-
recordFinalError(
|
|
547
|
+
recordFinalError(terminalFailureStatus, lastAttemptLabel, "translation", {
|
|
548
|
+
requestId: ctx.requestId,
|
|
549
|
+
errorType: "generation_error",
|
|
550
|
+
terminalOutcome: "handler_error",
|
|
551
|
+
message: lastAttemptError,
|
|
552
|
+
});
|
|
509
553
|
tracer?.setError("generation_error", lastAttemptError.slice(0, 500));
|
|
510
|
-
tracer?.end(
|
|
554
|
+
tracer?.end(terminalFailureStatus, Date.now() - requestStartTime);
|
|
511
555
|
const traceCtx = tracer?.getTraceContext();
|
|
512
556
|
logRequest({
|
|
513
557
|
timestamp: new Date().toISOString(),
|
|
@@ -519,7 +563,7 @@ export async function handleTranslatedJsonRequest(args) {
|
|
|
519
563
|
toolCount: Object.keys(parsed.tools).length,
|
|
520
564
|
account: "translation",
|
|
521
565
|
accountType: "translation",
|
|
522
|
-
responseStatus:
|
|
566
|
+
responseStatus: terminalFailureStatus,
|
|
523
567
|
responseTimeMs: Date.now() - requestStartTime,
|
|
524
568
|
errorType: "generation_error",
|
|
525
569
|
errorMessage: lastAttemptError.slice(0, 500),
|
|
@@ -60,7 +60,11 @@ export function createRawStreamCapture() {
|
|
|
60
60
|
},
|
|
61
61
|
});
|
|
62
62
|
const innerWriter = transform.writable.getWriter();
|
|
63
|
+
let writableController;
|
|
63
64
|
const writable = new WritableStream({
|
|
65
|
+
start(controller) {
|
|
66
|
+
writableController = controller;
|
|
67
|
+
},
|
|
64
68
|
write(chunk) {
|
|
65
69
|
return innerWriter.write(chunk);
|
|
66
70
|
},
|
|
@@ -72,9 +76,51 @@ export function createRawStreamCapture() {
|
|
|
72
76
|
return innerWriter.abort(reason);
|
|
73
77
|
},
|
|
74
78
|
});
|
|
79
|
+
// A downstream reader cancellation errors the TransformStream's writable
|
|
80
|
+
// side, but this wrapper otherwise hides that state from the upstream pipe.
|
|
81
|
+
// Mirror it onto the wrapper so cancellation reaches the source reader.
|
|
82
|
+
void innerWriter.closed.catch((reason) => {
|
|
83
|
+
settle();
|
|
84
|
+
try {
|
|
85
|
+
writableController.error(reason);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
// The wrapper may already be closing or aborted.
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
const innerReader = transform.readable.getReader();
|
|
92
|
+
const readable = new ReadableStream({
|
|
93
|
+
async pull(controller) {
|
|
94
|
+
try {
|
|
95
|
+
const { done, value } = await innerReader.read();
|
|
96
|
+
if (done) {
|
|
97
|
+
controller.close();
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
controller.enqueue(value);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
controller.error(error);
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
async cancel(reason) {
|
|
108
|
+
settle();
|
|
109
|
+
try {
|
|
110
|
+
writableController.error(reason);
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
// The wrapper may already be closing or aborted.
|
|
114
|
+
}
|
|
115
|
+
await Promise.allSettled([
|
|
116
|
+
innerReader.cancel(reason),
|
|
117
|
+
innerWriter.abort(reason),
|
|
118
|
+
]);
|
|
119
|
+
},
|
|
120
|
+
});
|
|
75
121
|
return {
|
|
76
122
|
stream: {
|
|
77
|
-
readable
|
|
123
|
+
readable,
|
|
78
124
|
writable,
|
|
79
125
|
},
|
|
80
126
|
capture,
|