@juspay/neurolink 9.70.2 → 9.70.4
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 +290 -290
- package/dist/lib/providers/googleVertex.js +30 -36
- package/dist/lib/utils/imageDetection.d.ts +15 -0
- package/dist/lib/utils/imageDetection.js +53 -0
- package/dist/lib/utils/json/coerce.js +40 -9
- package/dist/providers/googleVertex.js +30 -36
- package/dist/utils/imageDetection.d.ts +15 -0
- package/dist/utils/imageDetection.js +52 -0
- package/dist/utils/json/coerce.js +40 -9
- package/package.json +3 -3
|
@@ -14,6 +14,7 @@ import { FileDetector } from "../utils/fileDetector.js";
|
|
|
14
14
|
import { processUnifiedFilesArray } from "../utils/messageBuilder.js";
|
|
15
15
|
import { logger } from "../utils/logger.js";
|
|
16
16
|
import { hasRestrictedOutputLimit, RESTRICTED_OUTPUT_TOKEN_LIMIT, toVertexAnthropicModelId, } from "../utils/modelDetection.js";
|
|
17
|
+
import { detectImageMimeType } from "../utils/imageDetection.js";
|
|
17
18
|
import { resolveClaudeMaxTokens } from "../utils/tokenLimits.js";
|
|
18
19
|
import { validateApiKey, createVertexProjectConfig, createGoogleAuthConfig, } from "../utils/providerConfig.js";
|
|
19
20
|
import { convertZodToJsonSchema, inlineJsonSchema, ensureNestedSchemaTypes, } from "../utils/schemaConversion.js";
|
|
@@ -948,10 +949,17 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
948
949
|
else {
|
|
949
950
|
// Assume base64 string
|
|
950
951
|
imageBuffer = Buffer.from(image, "base64");
|
|
952
|
+
// Sniff the real format from magic bytes — bare base64 carries no
|
|
953
|
+
// mime hint, and leaving the image/jpeg default makes Anthropic
|
|
954
|
+
// reject PNG/GIF/WebP with a media-type mismatch 400.
|
|
955
|
+
mimeType = this.detectImageType(imageBuffer);
|
|
951
956
|
}
|
|
952
957
|
}
|
|
953
958
|
else {
|
|
954
959
|
imageBuffer = image;
|
|
960
|
+
// Buffer input (e.g. Slack/REST uploads) carries no mime hint; sniff
|
|
961
|
+
// it instead of defaulting to image/jpeg (mislabels PNG -> 400).
|
|
962
|
+
mimeType = this.detectImageType(imageBuffer);
|
|
955
963
|
}
|
|
956
964
|
const base64Data = imageBuffer.toString("base64");
|
|
957
965
|
userParts.push({
|
|
@@ -1567,10 +1575,17 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
1567
1575
|
else {
|
|
1568
1576
|
// Assume base64 string
|
|
1569
1577
|
imageBuffer = Buffer.from(image, "base64");
|
|
1578
|
+
// Sniff the real format from magic bytes — bare base64 carries no
|
|
1579
|
+
// mime hint, and leaving the image/jpeg default makes Anthropic
|
|
1580
|
+
// reject PNG/GIF/WebP with a media-type mismatch 400.
|
|
1581
|
+
mimeType = this.detectImageType(imageBuffer);
|
|
1570
1582
|
}
|
|
1571
1583
|
}
|
|
1572
1584
|
else {
|
|
1573
1585
|
imageBuffer = image;
|
|
1586
|
+
// Buffer input (e.g. Slack/REST uploads) carries no mime hint; sniff
|
|
1587
|
+
// it instead of defaulting to image/jpeg (mislabels PNG -> 400).
|
|
1588
|
+
mimeType = this.detectImageType(imageBuffer);
|
|
1574
1589
|
}
|
|
1575
1590
|
const base64Data = imageBuffer.toString("base64");
|
|
1576
1591
|
userParts.push({
|
|
@@ -2184,10 +2199,17 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
2184
2199
|
else {
|
|
2185
2200
|
// Assume base64 string
|
|
2186
2201
|
imageBuffer = Buffer.from(image, "base64");
|
|
2202
|
+
// Sniff the real format from magic bytes — bare base64 carries no
|
|
2203
|
+
// mime hint, and leaving the image/jpeg default makes Anthropic
|
|
2204
|
+
// reject PNG/GIF/WebP with a media-type mismatch 400.
|
|
2205
|
+
mimeType = this.detectImageType(imageBuffer);
|
|
2187
2206
|
}
|
|
2188
2207
|
}
|
|
2189
2208
|
else {
|
|
2190
2209
|
imageBuffer = image;
|
|
2210
|
+
// Buffer input (e.g. Slack/REST uploads) carries no mime hint; sniff
|
|
2211
|
+
// it instead of defaulting to image/jpeg (mislabels PNG -> 400).
|
|
2212
|
+
mimeType = this.detectImageType(imageBuffer);
|
|
2191
2213
|
}
|
|
2192
2214
|
const base64Data = imageBuffer.toString("base64");
|
|
2193
2215
|
userContentParts.push({
|
|
@@ -2713,10 +2735,17 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
2713
2735
|
else {
|
|
2714
2736
|
// Assume base64 string
|
|
2715
2737
|
imageBuffer = Buffer.from(image, "base64");
|
|
2738
|
+
// Sniff the real format from magic bytes — bare base64 carries no
|
|
2739
|
+
// mime hint, and leaving the image/jpeg default makes Anthropic
|
|
2740
|
+
// reject PNG/GIF/WebP with a media-type mismatch 400.
|
|
2741
|
+
mimeType = this.detectImageType(imageBuffer);
|
|
2716
2742
|
}
|
|
2717
2743
|
}
|
|
2718
2744
|
else {
|
|
2719
2745
|
imageBuffer = image;
|
|
2746
|
+
// Buffer input (e.g. Slack/REST uploads) carries no mime hint; sniff
|
|
2747
|
+
// it instead of defaulting to image/jpeg (mislabels PNG -> 400).
|
|
2748
|
+
mimeType = this.detectImageType(imageBuffer);
|
|
2720
2749
|
}
|
|
2721
2750
|
const base64Data = imageBuffer.toString("base64");
|
|
2722
2751
|
userContentParts.push({
|
|
@@ -4142,42 +4171,7 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
4142
4171
|
* Detect image MIME type from buffer
|
|
4143
4172
|
*/
|
|
4144
4173
|
detectImageType(buffer) {
|
|
4145
|
-
|
|
4146
|
-
if (buffer.length >= 8 &&
|
|
4147
|
-
buffer[0] === 0x89 &&
|
|
4148
|
-
buffer[1] === 0x50 &&
|
|
4149
|
-
buffer[2] === 0x4e &&
|
|
4150
|
-
buffer[3] === 0x47) {
|
|
4151
|
-
return "image/png";
|
|
4152
|
-
}
|
|
4153
|
-
// Check JPEG signature
|
|
4154
|
-
if (buffer.length >= 3 &&
|
|
4155
|
-
buffer[0] === 0xff &&
|
|
4156
|
-
buffer[1] === 0xd8 &&
|
|
4157
|
-
buffer[2] === 0xff) {
|
|
4158
|
-
return "image/jpeg";
|
|
4159
|
-
}
|
|
4160
|
-
// Check WebP signature
|
|
4161
|
-
if (buffer.length >= 12 &&
|
|
4162
|
-
buffer[0] === 0x52 &&
|
|
4163
|
-
buffer[1] === 0x49 &&
|
|
4164
|
-
buffer[2] === 0x46 &&
|
|
4165
|
-
buffer[3] === 0x46 &&
|
|
4166
|
-
buffer[8] === 0x57 &&
|
|
4167
|
-
buffer[9] === 0x45 &&
|
|
4168
|
-
buffer[10] === 0x42 &&
|
|
4169
|
-
buffer[11] === 0x50) {
|
|
4170
|
-
return "image/webp";
|
|
4171
|
-
}
|
|
4172
|
-
// Check GIF signature
|
|
4173
|
-
if (buffer.length >= 6 &&
|
|
4174
|
-
buffer[0] === 0x47 &&
|
|
4175
|
-
buffer[1] === 0x49 &&
|
|
4176
|
-
buffer[2] === 0x46) {
|
|
4177
|
-
return "image/gif";
|
|
4178
|
-
}
|
|
4179
|
-
// Default to PNG if unknown
|
|
4180
|
-
return "image/png";
|
|
4174
|
+
return detectImageMimeType(buffer);
|
|
4181
4175
|
}
|
|
4182
4176
|
/**
|
|
4183
4177
|
* Estimate token count from text (simple character-based estimation)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image format detection from magic bytes.
|
|
3
|
+
*
|
|
4
|
+
* The native Vertex+Anthropic image block needs the correct `mimeType` for
|
|
5
|
+
* each inline image. Buffer and bare-base64 inputs (e.g. Slack / REST uploads)
|
|
6
|
+
* carry no mime hint, so the format must be sniffed from the leading bytes —
|
|
7
|
+
* otherwise a wrong default (historically `image/jpeg`) makes Anthropic reject
|
|
8
|
+
* PNG/GIF/WebP with a media-type mismatch 400.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Detect an image's MIME type from its magic bytes. Returns `image/png` for
|
|
12
|
+
* buffers that match no known signature (the safest neutral default for the
|
|
13
|
+
* Vertex image path).
|
|
14
|
+
*/
|
|
15
|
+
export declare function detectImageMimeType(buffer: Buffer): string;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image format detection from magic bytes.
|
|
3
|
+
*
|
|
4
|
+
* The native Vertex+Anthropic image block needs the correct `mimeType` for
|
|
5
|
+
* each inline image. Buffer and bare-base64 inputs (e.g. Slack / REST uploads)
|
|
6
|
+
* carry no mime hint, so the format must be sniffed from the leading bytes —
|
|
7
|
+
* otherwise a wrong default (historically `image/jpeg`) makes Anthropic reject
|
|
8
|
+
* PNG/GIF/WebP with a media-type mismatch 400.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Detect an image's MIME type from its magic bytes. Returns `image/png` for
|
|
12
|
+
* buffers that match no known signature (the safest neutral default for the
|
|
13
|
+
* Vertex image path).
|
|
14
|
+
*/
|
|
15
|
+
export function detectImageMimeType(buffer) {
|
|
16
|
+
// PNG: 89 50 4E 47
|
|
17
|
+
if (buffer.length >= 8 &&
|
|
18
|
+
buffer[0] === 0x89 &&
|
|
19
|
+
buffer[1] === 0x50 &&
|
|
20
|
+
buffer[2] === 0x4e &&
|
|
21
|
+
buffer[3] === 0x47) {
|
|
22
|
+
return "image/png";
|
|
23
|
+
}
|
|
24
|
+
// JPEG: FF D8 FF
|
|
25
|
+
if (buffer.length >= 3 &&
|
|
26
|
+
buffer[0] === 0xff &&
|
|
27
|
+
buffer[1] === 0xd8 &&
|
|
28
|
+
buffer[2] === 0xff) {
|
|
29
|
+
return "image/jpeg";
|
|
30
|
+
}
|
|
31
|
+
// WebP: "RIFF"...."WEBP"
|
|
32
|
+
if (buffer.length >= 12 &&
|
|
33
|
+
buffer[0] === 0x52 &&
|
|
34
|
+
buffer[1] === 0x49 &&
|
|
35
|
+
buffer[2] === 0x46 &&
|
|
36
|
+
buffer[3] === 0x46 &&
|
|
37
|
+
buffer[8] === 0x57 &&
|
|
38
|
+
buffer[9] === 0x45 &&
|
|
39
|
+
buffer[10] === 0x42 &&
|
|
40
|
+
buffer[11] === 0x50) {
|
|
41
|
+
return "image/webp";
|
|
42
|
+
}
|
|
43
|
+
// GIF: "GIF"
|
|
44
|
+
if (buffer.length >= 6 &&
|
|
45
|
+
buffer[0] === 0x47 &&
|
|
46
|
+
buffer[1] === 0x49 &&
|
|
47
|
+
buffer[2] === 0x46) {
|
|
48
|
+
return "image/gif";
|
|
49
|
+
}
|
|
50
|
+
// Unknown — neutral default.
|
|
51
|
+
return "image/png";
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=imageDetection.js.map
|
|
@@ -93,8 +93,32 @@ export function coerceJsonToSchema(text, schema) {
|
|
|
93
93
|
if (firstOpen >= 0) {
|
|
94
94
|
candidates.push({ text: text.slice(firstOpen), truncated: true });
|
|
95
95
|
}
|
|
96
|
+
// JSON-string-literal wrapper: some providers double-encode and return the
|
|
97
|
+
// object as a JSON *string* (e.g. `"{\"k\":1}"`). Unwrap one layer and add
|
|
98
|
+
// the inner text's balanced spans as candidates so the object is recovered.
|
|
99
|
+
const literal = text.trim();
|
|
100
|
+
if (literal.length > 1 && literal.startsWith('"') && literal.endsWith('"')) {
|
|
101
|
+
try {
|
|
102
|
+
const inner = JSON.parse(literal);
|
|
103
|
+
if (typeof inner === "string") {
|
|
104
|
+
let innerFrom = 0;
|
|
105
|
+
for (;;) {
|
|
106
|
+
const innerSpan = nextBalancedJsonSpan(inner, innerFrom);
|
|
107
|
+
if (!innerSpan) {
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
candidates.push({ text: innerSpan.span, truncated: false });
|
|
111
|
+
innerFrom = innerSpan.end;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
// not a string literal — ignore
|
|
117
|
+
}
|
|
118
|
+
}
|
|
96
119
|
let firstValid;
|
|
97
|
-
|
|
120
|
+
const schemaValid = [];
|
|
121
|
+
const hasSchema = !!(schema && hasSafeParse(schema));
|
|
98
122
|
const seen = new Set();
|
|
99
123
|
for (const candidate of candidates) {
|
|
100
124
|
if (seen.has(candidate.text)) {
|
|
@@ -115,18 +139,25 @@ export function coerceJsonToSchema(text, schema) {
|
|
|
115
139
|
if (firstValid === undefined) {
|
|
116
140
|
firstValid = record;
|
|
117
141
|
}
|
|
118
|
-
if (
|
|
119
|
-
const safeParseable = schema;
|
|
120
|
-
if (safeParseable.safeParse(outcome.value).success) {
|
|
121
|
-
schemaMatch = record;
|
|
122
|
-
break;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
else {
|
|
142
|
+
if (!hasSchema) {
|
|
126
143
|
// No Zod schema to discriminate — first parseable object wins.
|
|
127
144
|
break;
|
|
128
145
|
}
|
|
146
|
+
const safeParseable = schema;
|
|
147
|
+
if (safeParseable.safeParse(outcome.value).success) {
|
|
148
|
+
schemaValid.push(record);
|
|
149
|
+
}
|
|
129
150
|
}
|
|
151
|
+
// Among schema-valid candidates prefer the MOST COMPLETE one. With nullable
|
|
152
|
+
// fields a lean object (e.g. `{summary, attachment: null}`) validates
|
|
153
|
+
// alongside the full object, so breaking on the first match would drop the
|
|
154
|
+
// richer payload (the classic preamble-then-real-answer case). Pick the
|
|
155
|
+
// candidate whose serialized form carries the most content.
|
|
156
|
+
const schemaMatch = schemaValid.length > 0
|
|
157
|
+
? schemaValid.reduce((best, cur) => JSON.stringify(cur.value).length > JSON.stringify(best.value).length
|
|
158
|
+
? cur
|
|
159
|
+
: best)
|
|
160
|
+
: undefined;
|
|
130
161
|
const chosen = schemaMatch ?? firstValid;
|
|
131
162
|
if (chosen === undefined) {
|
|
132
163
|
return null;
|
|
@@ -14,6 +14,7 @@ import { FileDetector } from "../utils/fileDetector.js";
|
|
|
14
14
|
import { processUnifiedFilesArray } from "../utils/messageBuilder.js";
|
|
15
15
|
import { logger } from "../utils/logger.js";
|
|
16
16
|
import { hasRestrictedOutputLimit, RESTRICTED_OUTPUT_TOKEN_LIMIT, toVertexAnthropicModelId, } from "../utils/modelDetection.js";
|
|
17
|
+
import { detectImageMimeType } from "../utils/imageDetection.js";
|
|
17
18
|
import { resolveClaudeMaxTokens } from "../utils/tokenLimits.js";
|
|
18
19
|
import { validateApiKey, createVertexProjectConfig, createGoogleAuthConfig, } from "../utils/providerConfig.js";
|
|
19
20
|
import { convertZodToJsonSchema, inlineJsonSchema, ensureNestedSchemaTypes, } from "../utils/schemaConversion.js";
|
|
@@ -948,10 +949,17 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
948
949
|
else {
|
|
949
950
|
// Assume base64 string
|
|
950
951
|
imageBuffer = Buffer.from(image, "base64");
|
|
952
|
+
// Sniff the real format from magic bytes — bare base64 carries no
|
|
953
|
+
// mime hint, and leaving the image/jpeg default makes Anthropic
|
|
954
|
+
// reject PNG/GIF/WebP with a media-type mismatch 400.
|
|
955
|
+
mimeType = this.detectImageType(imageBuffer);
|
|
951
956
|
}
|
|
952
957
|
}
|
|
953
958
|
else {
|
|
954
959
|
imageBuffer = image;
|
|
960
|
+
// Buffer input (e.g. Slack/REST uploads) carries no mime hint; sniff
|
|
961
|
+
// it instead of defaulting to image/jpeg (mislabels PNG -> 400).
|
|
962
|
+
mimeType = this.detectImageType(imageBuffer);
|
|
955
963
|
}
|
|
956
964
|
const base64Data = imageBuffer.toString("base64");
|
|
957
965
|
userParts.push({
|
|
@@ -1567,10 +1575,17 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
1567
1575
|
else {
|
|
1568
1576
|
// Assume base64 string
|
|
1569
1577
|
imageBuffer = Buffer.from(image, "base64");
|
|
1578
|
+
// Sniff the real format from magic bytes — bare base64 carries no
|
|
1579
|
+
// mime hint, and leaving the image/jpeg default makes Anthropic
|
|
1580
|
+
// reject PNG/GIF/WebP with a media-type mismatch 400.
|
|
1581
|
+
mimeType = this.detectImageType(imageBuffer);
|
|
1570
1582
|
}
|
|
1571
1583
|
}
|
|
1572
1584
|
else {
|
|
1573
1585
|
imageBuffer = image;
|
|
1586
|
+
// Buffer input (e.g. Slack/REST uploads) carries no mime hint; sniff
|
|
1587
|
+
// it instead of defaulting to image/jpeg (mislabels PNG -> 400).
|
|
1588
|
+
mimeType = this.detectImageType(imageBuffer);
|
|
1574
1589
|
}
|
|
1575
1590
|
const base64Data = imageBuffer.toString("base64");
|
|
1576
1591
|
userParts.push({
|
|
@@ -2184,10 +2199,17 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
2184
2199
|
else {
|
|
2185
2200
|
// Assume base64 string
|
|
2186
2201
|
imageBuffer = Buffer.from(image, "base64");
|
|
2202
|
+
// Sniff the real format from magic bytes — bare base64 carries no
|
|
2203
|
+
// mime hint, and leaving the image/jpeg default makes Anthropic
|
|
2204
|
+
// reject PNG/GIF/WebP with a media-type mismatch 400.
|
|
2205
|
+
mimeType = this.detectImageType(imageBuffer);
|
|
2187
2206
|
}
|
|
2188
2207
|
}
|
|
2189
2208
|
else {
|
|
2190
2209
|
imageBuffer = image;
|
|
2210
|
+
// Buffer input (e.g. Slack/REST uploads) carries no mime hint; sniff
|
|
2211
|
+
// it instead of defaulting to image/jpeg (mislabels PNG -> 400).
|
|
2212
|
+
mimeType = this.detectImageType(imageBuffer);
|
|
2191
2213
|
}
|
|
2192
2214
|
const base64Data = imageBuffer.toString("base64");
|
|
2193
2215
|
userContentParts.push({
|
|
@@ -2713,10 +2735,17 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
2713
2735
|
else {
|
|
2714
2736
|
// Assume base64 string
|
|
2715
2737
|
imageBuffer = Buffer.from(image, "base64");
|
|
2738
|
+
// Sniff the real format from magic bytes — bare base64 carries no
|
|
2739
|
+
// mime hint, and leaving the image/jpeg default makes Anthropic
|
|
2740
|
+
// reject PNG/GIF/WebP with a media-type mismatch 400.
|
|
2741
|
+
mimeType = this.detectImageType(imageBuffer);
|
|
2716
2742
|
}
|
|
2717
2743
|
}
|
|
2718
2744
|
else {
|
|
2719
2745
|
imageBuffer = image;
|
|
2746
|
+
// Buffer input (e.g. Slack/REST uploads) carries no mime hint; sniff
|
|
2747
|
+
// it instead of defaulting to image/jpeg (mislabels PNG -> 400).
|
|
2748
|
+
mimeType = this.detectImageType(imageBuffer);
|
|
2720
2749
|
}
|
|
2721
2750
|
const base64Data = imageBuffer.toString("base64");
|
|
2722
2751
|
userContentParts.push({
|
|
@@ -4142,42 +4171,7 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
4142
4171
|
* Detect image MIME type from buffer
|
|
4143
4172
|
*/
|
|
4144
4173
|
detectImageType(buffer) {
|
|
4145
|
-
|
|
4146
|
-
if (buffer.length >= 8 &&
|
|
4147
|
-
buffer[0] === 0x89 &&
|
|
4148
|
-
buffer[1] === 0x50 &&
|
|
4149
|
-
buffer[2] === 0x4e &&
|
|
4150
|
-
buffer[3] === 0x47) {
|
|
4151
|
-
return "image/png";
|
|
4152
|
-
}
|
|
4153
|
-
// Check JPEG signature
|
|
4154
|
-
if (buffer.length >= 3 &&
|
|
4155
|
-
buffer[0] === 0xff &&
|
|
4156
|
-
buffer[1] === 0xd8 &&
|
|
4157
|
-
buffer[2] === 0xff) {
|
|
4158
|
-
return "image/jpeg";
|
|
4159
|
-
}
|
|
4160
|
-
// Check WebP signature
|
|
4161
|
-
if (buffer.length >= 12 &&
|
|
4162
|
-
buffer[0] === 0x52 &&
|
|
4163
|
-
buffer[1] === 0x49 &&
|
|
4164
|
-
buffer[2] === 0x46 &&
|
|
4165
|
-
buffer[3] === 0x46 &&
|
|
4166
|
-
buffer[8] === 0x57 &&
|
|
4167
|
-
buffer[9] === 0x45 &&
|
|
4168
|
-
buffer[10] === 0x42 &&
|
|
4169
|
-
buffer[11] === 0x50) {
|
|
4170
|
-
return "image/webp";
|
|
4171
|
-
}
|
|
4172
|
-
// Check GIF signature
|
|
4173
|
-
if (buffer.length >= 6 &&
|
|
4174
|
-
buffer[0] === 0x47 &&
|
|
4175
|
-
buffer[1] === 0x49 &&
|
|
4176
|
-
buffer[2] === 0x46) {
|
|
4177
|
-
return "image/gif";
|
|
4178
|
-
}
|
|
4179
|
-
// Default to PNG if unknown
|
|
4180
|
-
return "image/png";
|
|
4174
|
+
return detectImageMimeType(buffer);
|
|
4181
4175
|
}
|
|
4182
4176
|
/**
|
|
4183
4177
|
* Estimate token count from text (simple character-based estimation)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image format detection from magic bytes.
|
|
3
|
+
*
|
|
4
|
+
* The native Vertex+Anthropic image block needs the correct `mimeType` for
|
|
5
|
+
* each inline image. Buffer and bare-base64 inputs (e.g. Slack / REST uploads)
|
|
6
|
+
* carry no mime hint, so the format must be sniffed from the leading bytes —
|
|
7
|
+
* otherwise a wrong default (historically `image/jpeg`) makes Anthropic reject
|
|
8
|
+
* PNG/GIF/WebP with a media-type mismatch 400.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Detect an image's MIME type from its magic bytes. Returns `image/png` for
|
|
12
|
+
* buffers that match no known signature (the safest neutral default for the
|
|
13
|
+
* Vertex image path).
|
|
14
|
+
*/
|
|
15
|
+
export declare function detectImageMimeType(buffer: Buffer): string;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image format detection from magic bytes.
|
|
3
|
+
*
|
|
4
|
+
* The native Vertex+Anthropic image block needs the correct `mimeType` for
|
|
5
|
+
* each inline image. Buffer and bare-base64 inputs (e.g. Slack / REST uploads)
|
|
6
|
+
* carry no mime hint, so the format must be sniffed from the leading bytes —
|
|
7
|
+
* otherwise a wrong default (historically `image/jpeg`) makes Anthropic reject
|
|
8
|
+
* PNG/GIF/WebP with a media-type mismatch 400.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Detect an image's MIME type from its magic bytes. Returns `image/png` for
|
|
12
|
+
* buffers that match no known signature (the safest neutral default for the
|
|
13
|
+
* Vertex image path).
|
|
14
|
+
*/
|
|
15
|
+
export function detectImageMimeType(buffer) {
|
|
16
|
+
// PNG: 89 50 4E 47
|
|
17
|
+
if (buffer.length >= 8 &&
|
|
18
|
+
buffer[0] === 0x89 &&
|
|
19
|
+
buffer[1] === 0x50 &&
|
|
20
|
+
buffer[2] === 0x4e &&
|
|
21
|
+
buffer[3] === 0x47) {
|
|
22
|
+
return "image/png";
|
|
23
|
+
}
|
|
24
|
+
// JPEG: FF D8 FF
|
|
25
|
+
if (buffer.length >= 3 &&
|
|
26
|
+
buffer[0] === 0xff &&
|
|
27
|
+
buffer[1] === 0xd8 &&
|
|
28
|
+
buffer[2] === 0xff) {
|
|
29
|
+
return "image/jpeg";
|
|
30
|
+
}
|
|
31
|
+
// WebP: "RIFF"...."WEBP"
|
|
32
|
+
if (buffer.length >= 12 &&
|
|
33
|
+
buffer[0] === 0x52 &&
|
|
34
|
+
buffer[1] === 0x49 &&
|
|
35
|
+
buffer[2] === 0x46 &&
|
|
36
|
+
buffer[3] === 0x46 &&
|
|
37
|
+
buffer[8] === 0x57 &&
|
|
38
|
+
buffer[9] === 0x45 &&
|
|
39
|
+
buffer[10] === 0x42 &&
|
|
40
|
+
buffer[11] === 0x50) {
|
|
41
|
+
return "image/webp";
|
|
42
|
+
}
|
|
43
|
+
// GIF: "GIF"
|
|
44
|
+
if (buffer.length >= 6 &&
|
|
45
|
+
buffer[0] === 0x47 &&
|
|
46
|
+
buffer[1] === 0x49 &&
|
|
47
|
+
buffer[2] === 0x46) {
|
|
48
|
+
return "image/gif";
|
|
49
|
+
}
|
|
50
|
+
// Unknown — neutral default.
|
|
51
|
+
return "image/png";
|
|
52
|
+
}
|
|
@@ -93,8 +93,32 @@ export function coerceJsonToSchema(text, schema) {
|
|
|
93
93
|
if (firstOpen >= 0) {
|
|
94
94
|
candidates.push({ text: text.slice(firstOpen), truncated: true });
|
|
95
95
|
}
|
|
96
|
+
// JSON-string-literal wrapper: some providers double-encode and return the
|
|
97
|
+
// object as a JSON *string* (e.g. `"{\"k\":1}"`). Unwrap one layer and add
|
|
98
|
+
// the inner text's balanced spans as candidates so the object is recovered.
|
|
99
|
+
const literal = text.trim();
|
|
100
|
+
if (literal.length > 1 && literal.startsWith('"') && literal.endsWith('"')) {
|
|
101
|
+
try {
|
|
102
|
+
const inner = JSON.parse(literal);
|
|
103
|
+
if (typeof inner === "string") {
|
|
104
|
+
let innerFrom = 0;
|
|
105
|
+
for (;;) {
|
|
106
|
+
const innerSpan = nextBalancedJsonSpan(inner, innerFrom);
|
|
107
|
+
if (!innerSpan) {
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
candidates.push({ text: innerSpan.span, truncated: false });
|
|
111
|
+
innerFrom = innerSpan.end;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
// not a string literal — ignore
|
|
117
|
+
}
|
|
118
|
+
}
|
|
96
119
|
let firstValid;
|
|
97
|
-
|
|
120
|
+
const schemaValid = [];
|
|
121
|
+
const hasSchema = !!(schema && hasSafeParse(schema));
|
|
98
122
|
const seen = new Set();
|
|
99
123
|
for (const candidate of candidates) {
|
|
100
124
|
if (seen.has(candidate.text)) {
|
|
@@ -115,18 +139,25 @@ export function coerceJsonToSchema(text, schema) {
|
|
|
115
139
|
if (firstValid === undefined) {
|
|
116
140
|
firstValid = record;
|
|
117
141
|
}
|
|
118
|
-
if (
|
|
119
|
-
const safeParseable = schema;
|
|
120
|
-
if (safeParseable.safeParse(outcome.value).success) {
|
|
121
|
-
schemaMatch = record;
|
|
122
|
-
break;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
else {
|
|
142
|
+
if (!hasSchema) {
|
|
126
143
|
// No Zod schema to discriminate — first parseable object wins.
|
|
127
144
|
break;
|
|
128
145
|
}
|
|
146
|
+
const safeParseable = schema;
|
|
147
|
+
if (safeParseable.safeParse(outcome.value).success) {
|
|
148
|
+
schemaValid.push(record);
|
|
149
|
+
}
|
|
129
150
|
}
|
|
151
|
+
// Among schema-valid candidates prefer the MOST COMPLETE one. With nullable
|
|
152
|
+
// fields a lean object (e.g. `{summary, attachment: null}`) validates
|
|
153
|
+
// alongside the full object, so breaking on the first match would drop the
|
|
154
|
+
// richer payload (the classic preamble-then-real-answer case). Pick the
|
|
155
|
+
// candidate whose serialized form carries the most content.
|
|
156
|
+
const schemaMatch = schemaValid.length > 0
|
|
157
|
+
? schemaValid.reduce((best, cur) => JSON.stringify(cur.value).length > JSON.stringify(best.value).length
|
|
158
|
+
? cur
|
|
159
|
+
: best)
|
|
160
|
+
: undefined;
|
|
130
161
|
const chosen = schemaMatch ?? firstValid;
|
|
131
162
|
if (chosen === undefined) {
|
|
132
163
|
return null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juspay/neurolink",
|
|
3
|
-
"version": "9.70.
|
|
3
|
+
"version": "9.70.4",
|
|
4
4
|
"packageManager": "pnpm@10.15.1",
|
|
5
5
|
"description": "Universal AI Development Platform with working MCP integration, multi-provider support, voice (TTS/STT/realtime), and professional CLI. 58+ external MCP servers discoverable, multimodal file processing, RAG pipelines. Build, test, and deploy AI applications with 21+ providers: OpenAI, Anthropic, Google AI Studio, Google Vertex, AWS Bedrock, Azure OpenAI, Mistral, LiteLLM, SageMaker, Hugging Face, Ollama, OpenAI-compatible, OpenRouter, DeepSeek, NVIDIA NIM, LM Studio, llama.cpp, plus voice (OpenAI TTS, ElevenLabs, Deepgram, Azure Speech).",
|
|
6
6
|
"author": {
|
|
@@ -453,7 +453,7 @@
|
|
|
453
453
|
"@vitest/coverage-v8": "^4.1.0",
|
|
454
454
|
"concurrently": "^9.2.1",
|
|
455
455
|
"conventional-changelog-conventionalcommits": "^9.1.0",
|
|
456
|
-
"esbuild": "^0.
|
|
456
|
+
"esbuild": "^0.28.1",
|
|
457
457
|
"eslint": "^10.0.2",
|
|
458
458
|
"husky": "^9.1.7",
|
|
459
459
|
"js-yaml": "^4.1.1",
|
|
@@ -465,7 +465,7 @@
|
|
|
465
465
|
"react": "^19.2.4",
|
|
466
466
|
"react-dom": "^19.2.4",
|
|
467
467
|
"semantic-release": "^25.0.3",
|
|
468
|
-
"shell-quote": "^1.8.
|
|
468
|
+
"shell-quote": "^1.8.4",
|
|
469
469
|
"svelte": "^5.55.7",
|
|
470
470
|
"svelte-check": "^4.4.4",
|
|
471
471
|
"ts-morph": "^24.0.0",
|