@juspay/neurolink 9.70.1 → 9.70.3
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 +282 -282
- package/dist/lib/providers/googleVertex.js +45 -43
- package/dist/lib/utils/imageDetection.d.ts +15 -0
- package/dist/lib/utils/imageDetection.js +53 -0
- package/dist/lib/utils/modelDetection.d.ts +17 -0
- package/dist/lib/utils/modelDetection.js +23 -0
- package/dist/providers/googleVertex.js +45 -43
- package/dist/utils/imageDetection.d.ts +15 -0
- package/dist/utils/imageDetection.js +52 -0
- package/dist/utils/modelDetection.d.ts +17 -0
- package/dist/utils/modelDetection.js +23 -0
- package/package.json +3 -3
|
@@ -13,7 +13,8 @@ import { ERROR_CODES, NeuroLinkError } from "../utils/errorHandling.js";
|
|
|
13
13
|
import { FileDetector } from "../utils/fileDetector.js";
|
|
14
14
|
import { processUnifiedFilesArray } from "../utils/messageBuilder.js";
|
|
15
15
|
import { logger } from "../utils/logger.js";
|
|
16
|
-
import { hasRestrictedOutputLimit, RESTRICTED_OUTPUT_TOKEN_LIMIT, } from "../utils/modelDetection.js";
|
|
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({
|
|
@@ -2058,7 +2073,7 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
2058
2073
|
* This bypasses @ai-sdk/google-vertex completely and uses Anthropic's native SDK
|
|
2059
2074
|
*/
|
|
2060
2075
|
async executeNativeAnthropicStream(options) {
|
|
2061
|
-
const modelName = options.model || this.modelName || "claude-sonnet-4-5@20250929";
|
|
2076
|
+
const modelName = toVertexAnthropicModelId(options.model || this.modelName || "claude-sonnet-4-5@20250929");
|
|
2062
2077
|
const startTime = Date.now();
|
|
2063
2078
|
const streamTimeoutMs = parseTimeout(options.timeout) ?? 300_000;
|
|
2064
2079
|
const client = await this.createAnthropicVertexClient(streamTimeoutMs);
|
|
@@ -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({
|
|
@@ -2231,7 +2253,11 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
2231
2253
|
const legacyTool = tool;
|
|
2232
2254
|
const toolParams = legacyTool.parameters || tool.inputSchema;
|
|
2233
2255
|
if (toolParams) {
|
|
2234
|
-
|
|
2256
|
+
// Anthropic validates input_schema as JSON Schema draft 2020-12 and
|
|
2257
|
+
// rejects OpenAPI-3 dialect output (e.g. `nullable: true`) with a
|
|
2258
|
+
// 400 — use the default JSON Schema target, matching the direct
|
|
2259
|
+
// anthropic provider. The Gemini paths keep "openApi3".
|
|
2260
|
+
const jsonSchema = convertZodToJsonSchema(toolParams);
|
|
2235
2261
|
const inlined = inlineJsonSchema(jsonSchema);
|
|
2236
2262
|
anthropicTool.input_schema = {
|
|
2237
2263
|
type: "object",
|
|
@@ -2257,7 +2283,7 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
2257
2283
|
if (streamOptions.schema) {
|
|
2258
2284
|
useFinalResultTool = true;
|
|
2259
2285
|
// Convert schema to JSON schema format
|
|
2260
|
-
const schemaAsJson = convertZodToJsonSchema(streamOptions.schema
|
|
2286
|
+
const schemaAsJson = convertZodToJsonSchema(streamOptions.schema);
|
|
2261
2287
|
const inlinedSchema = inlineJsonSchema(schemaAsJson);
|
|
2262
2288
|
if (inlinedSchema.$schema) {
|
|
2263
2289
|
delete inlinedSchema.$schema;
|
|
@@ -2580,7 +2606,7 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
2580
2606
|
* Execute generate using native @anthropic-ai/vertex-sdk for Claude models on Vertex AI
|
|
2581
2607
|
*/
|
|
2582
2608
|
async executeNativeAnthropicGenerate(options) {
|
|
2583
|
-
const modelName = options.model || this.modelName || "claude-sonnet-4-5@20250929";
|
|
2609
|
+
const modelName = toVertexAnthropicModelId(options.model || this.modelName || "claude-sonnet-4-5@20250929");
|
|
2584
2610
|
const startTime = Date.now();
|
|
2585
2611
|
const generateTimeoutMs = parseTimeout(options.timeout) ?? 300_000;
|
|
2586
2612
|
const client = await this.createAnthropicVertexClient(generateTimeoutMs);
|
|
@@ -2709,10 +2735,17 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
2709
2735
|
else {
|
|
2710
2736
|
// Assume base64 string
|
|
2711
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);
|
|
2712
2742
|
}
|
|
2713
2743
|
}
|
|
2714
2744
|
else {
|
|
2715
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);
|
|
2716
2749
|
}
|
|
2717
2750
|
const base64Data = imageBuffer.toString("base64");
|
|
2718
2751
|
userContentParts.push({
|
|
@@ -2760,7 +2793,11 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
2760
2793
|
const legacyTool = tool;
|
|
2761
2794
|
const toolParams = legacyTool.parameters || tool.inputSchema;
|
|
2762
2795
|
if (toolParams) {
|
|
2763
|
-
|
|
2796
|
+
// Anthropic validates input_schema as JSON Schema draft 2020-12 and
|
|
2797
|
+
// rejects OpenAPI-3 dialect output (e.g. `nullable: true`) with a
|
|
2798
|
+
// 400 — use the default JSON Schema target, matching the direct
|
|
2799
|
+
// anthropic provider. The Gemini paths keep "openApi3".
|
|
2800
|
+
const jsonSchema = convertZodToJsonSchema(toolParams);
|
|
2764
2801
|
const inlined = inlineJsonSchema(jsonSchema);
|
|
2765
2802
|
anthropicTool.input_schema = {
|
|
2766
2803
|
type: "object",
|
|
@@ -2781,7 +2818,7 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
2781
2818
|
if (options.schema) {
|
|
2782
2819
|
useFinalResultTool = true;
|
|
2783
2820
|
// Convert schema to JSON schema format
|
|
2784
|
-
const schemaAsJson = convertZodToJsonSchema(options.schema
|
|
2821
|
+
const schemaAsJson = convertZodToJsonSchema(options.schema);
|
|
2785
2822
|
const inlinedSchema = inlineJsonSchema(schemaAsJson);
|
|
2786
2823
|
if (inlinedSchema.$schema) {
|
|
2787
2824
|
delete inlinedSchema.$schema;
|
|
@@ -4134,42 +4171,7 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
4134
4171
|
* Detect image MIME type from buffer
|
|
4135
4172
|
*/
|
|
4136
4173
|
detectImageType(buffer) {
|
|
4137
|
-
|
|
4138
|
-
if (buffer.length >= 8 &&
|
|
4139
|
-
buffer[0] === 0x89 &&
|
|
4140
|
-
buffer[1] === 0x50 &&
|
|
4141
|
-
buffer[2] === 0x4e &&
|
|
4142
|
-
buffer[3] === 0x47) {
|
|
4143
|
-
return "image/png";
|
|
4144
|
-
}
|
|
4145
|
-
// Check JPEG signature
|
|
4146
|
-
if (buffer.length >= 3 &&
|
|
4147
|
-
buffer[0] === 0xff &&
|
|
4148
|
-
buffer[1] === 0xd8 &&
|
|
4149
|
-
buffer[2] === 0xff) {
|
|
4150
|
-
return "image/jpeg";
|
|
4151
|
-
}
|
|
4152
|
-
// Check WebP signature
|
|
4153
|
-
if (buffer.length >= 12 &&
|
|
4154
|
-
buffer[0] === 0x52 &&
|
|
4155
|
-
buffer[1] === 0x49 &&
|
|
4156
|
-
buffer[2] === 0x46 &&
|
|
4157
|
-
buffer[3] === 0x46 &&
|
|
4158
|
-
buffer[8] === 0x57 &&
|
|
4159
|
-
buffer[9] === 0x45 &&
|
|
4160
|
-
buffer[10] === 0x42 &&
|
|
4161
|
-
buffer[11] === 0x50) {
|
|
4162
|
-
return "image/webp";
|
|
4163
|
-
}
|
|
4164
|
-
// Check GIF signature
|
|
4165
|
-
if (buffer.length >= 6 &&
|
|
4166
|
-
buffer[0] === 0x47 &&
|
|
4167
|
-
buffer[1] === 0x49 &&
|
|
4168
|
-
buffer[2] === 0x46) {
|
|
4169
|
-
return "image/gif";
|
|
4170
|
-
}
|
|
4171
|
-
// Default to PNG if unknown
|
|
4172
|
-
return "image/png";
|
|
4174
|
+
return detectImageMimeType(buffer);
|
|
4173
4175
|
}
|
|
4174
4176
|
/**
|
|
4175
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
|
|
@@ -18,3 +18,20 @@ export declare function hasRestrictedOutputLimit(modelName: string): boolean;
|
|
|
18
18
|
* Get the max output tokens for a model (32768 for restricted models)
|
|
19
19
|
*/
|
|
20
20
|
export declare const RESTRICTED_OUTPUT_TOKEN_LIMIT = 32768;
|
|
21
|
+
/**
|
|
22
|
+
* Normalize an Anthropic-API-style Claude model ID to the Vertex publisher
|
|
23
|
+
* format.
|
|
24
|
+
*
|
|
25
|
+
* The Anthropic API dates models with a trailing dash segment
|
|
26
|
+
* ("claude-haiku-4-5-20251001") while Vertex publisher IDs separate the date
|
|
27
|
+
* with "@" ("claude-haiku-4-5@20251001"). Vertex rejects the dash form with a
|
|
28
|
+
* 404 (verified live against us-east5), so the native Vertex+Claude paths
|
|
29
|
+
* normalize before calling @anthropic-ai/vertex-sdk.
|
|
30
|
+
*
|
|
31
|
+
* Pass-through cases: IDs already in "@" form, bare aliases with no date
|
|
32
|
+
* ("claude-sonnet-4-6" — Vertex resolves these itself), and non-Claude models.
|
|
33
|
+
* Legacy v2-suffixed Vertex IDs ("claude-3-5-sonnet-v2@20241022") have no
|
|
34
|
+
* dash-date equivalent, so those legacy dash IDs stay out of scope: they 404
|
|
35
|
+
* today and still 404 after the transform — no regression either way.
|
|
36
|
+
*/
|
|
37
|
+
export declare function toVertexAnthropicModelId(modelName: string): string;
|
|
@@ -105,4 +105,27 @@ export function hasRestrictedOutputLimit(modelName) {
|
|
|
105
105
|
* Get the max output tokens for a model (32768 for restricted models)
|
|
106
106
|
*/
|
|
107
107
|
export const RESTRICTED_OUTPUT_TOKEN_LIMIT = 32768;
|
|
108
|
+
/**
|
|
109
|
+
* Normalize an Anthropic-API-style Claude model ID to the Vertex publisher
|
|
110
|
+
* format.
|
|
111
|
+
*
|
|
112
|
+
* The Anthropic API dates models with a trailing dash segment
|
|
113
|
+
* ("claude-haiku-4-5-20251001") while Vertex publisher IDs separate the date
|
|
114
|
+
* with "@" ("claude-haiku-4-5@20251001"). Vertex rejects the dash form with a
|
|
115
|
+
* 404 (verified live against us-east5), so the native Vertex+Claude paths
|
|
116
|
+
* normalize before calling @anthropic-ai/vertex-sdk.
|
|
117
|
+
*
|
|
118
|
+
* Pass-through cases: IDs already in "@" form, bare aliases with no date
|
|
119
|
+
* ("claude-sonnet-4-6" — Vertex resolves these itself), and non-Claude models.
|
|
120
|
+
* Legacy v2-suffixed Vertex IDs ("claude-3-5-sonnet-v2@20241022") have no
|
|
121
|
+
* dash-date equivalent, so those legacy dash IDs stay out of scope: they 404
|
|
122
|
+
* today and still 404 after the transform — no regression either way.
|
|
123
|
+
*/
|
|
124
|
+
export function toVertexAnthropicModelId(modelName) {
|
|
125
|
+
if (!modelName.startsWith("claude-") || modelName.includes("@")) {
|
|
126
|
+
return modelName;
|
|
127
|
+
}
|
|
128
|
+
const dashDate = modelName.match(/^(claude-[a-z0-9-]+)-(\d{8})$/);
|
|
129
|
+
return dashDate ? `${dashDate[1]}@${dashDate[2]}` : modelName;
|
|
130
|
+
}
|
|
108
131
|
//# sourceMappingURL=modelDetection.js.map
|
|
@@ -13,7 +13,8 @@ import { ERROR_CODES, NeuroLinkError } from "../utils/errorHandling.js";
|
|
|
13
13
|
import { FileDetector } from "../utils/fileDetector.js";
|
|
14
14
|
import { processUnifiedFilesArray } from "../utils/messageBuilder.js";
|
|
15
15
|
import { logger } from "../utils/logger.js";
|
|
16
|
-
import { hasRestrictedOutputLimit, RESTRICTED_OUTPUT_TOKEN_LIMIT, } from "../utils/modelDetection.js";
|
|
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({
|
|
@@ -2058,7 +2073,7 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
2058
2073
|
* This bypasses @ai-sdk/google-vertex completely and uses Anthropic's native SDK
|
|
2059
2074
|
*/
|
|
2060
2075
|
async executeNativeAnthropicStream(options) {
|
|
2061
|
-
const modelName = options.model || this.modelName || "claude-sonnet-4-5@20250929";
|
|
2076
|
+
const modelName = toVertexAnthropicModelId(options.model || this.modelName || "claude-sonnet-4-5@20250929");
|
|
2062
2077
|
const startTime = Date.now();
|
|
2063
2078
|
const streamTimeoutMs = parseTimeout(options.timeout) ?? 300_000;
|
|
2064
2079
|
const client = await this.createAnthropicVertexClient(streamTimeoutMs);
|
|
@@ -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({
|
|
@@ -2231,7 +2253,11 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
2231
2253
|
const legacyTool = tool;
|
|
2232
2254
|
const toolParams = legacyTool.parameters || tool.inputSchema;
|
|
2233
2255
|
if (toolParams) {
|
|
2234
|
-
|
|
2256
|
+
// Anthropic validates input_schema as JSON Schema draft 2020-12 and
|
|
2257
|
+
// rejects OpenAPI-3 dialect output (e.g. `nullable: true`) with a
|
|
2258
|
+
// 400 — use the default JSON Schema target, matching the direct
|
|
2259
|
+
// anthropic provider. The Gemini paths keep "openApi3".
|
|
2260
|
+
const jsonSchema = convertZodToJsonSchema(toolParams);
|
|
2235
2261
|
const inlined = inlineJsonSchema(jsonSchema);
|
|
2236
2262
|
anthropicTool.input_schema = {
|
|
2237
2263
|
type: "object",
|
|
@@ -2257,7 +2283,7 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
2257
2283
|
if (streamOptions.schema) {
|
|
2258
2284
|
useFinalResultTool = true;
|
|
2259
2285
|
// Convert schema to JSON schema format
|
|
2260
|
-
const schemaAsJson = convertZodToJsonSchema(streamOptions.schema
|
|
2286
|
+
const schemaAsJson = convertZodToJsonSchema(streamOptions.schema);
|
|
2261
2287
|
const inlinedSchema = inlineJsonSchema(schemaAsJson);
|
|
2262
2288
|
if (inlinedSchema.$schema) {
|
|
2263
2289
|
delete inlinedSchema.$schema;
|
|
@@ -2580,7 +2606,7 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
2580
2606
|
* Execute generate using native @anthropic-ai/vertex-sdk for Claude models on Vertex AI
|
|
2581
2607
|
*/
|
|
2582
2608
|
async executeNativeAnthropicGenerate(options) {
|
|
2583
|
-
const modelName = options.model || this.modelName || "claude-sonnet-4-5@20250929";
|
|
2609
|
+
const modelName = toVertexAnthropicModelId(options.model || this.modelName || "claude-sonnet-4-5@20250929");
|
|
2584
2610
|
const startTime = Date.now();
|
|
2585
2611
|
const generateTimeoutMs = parseTimeout(options.timeout) ?? 300_000;
|
|
2586
2612
|
const client = await this.createAnthropicVertexClient(generateTimeoutMs);
|
|
@@ -2709,10 +2735,17 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
2709
2735
|
else {
|
|
2710
2736
|
// Assume base64 string
|
|
2711
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);
|
|
2712
2742
|
}
|
|
2713
2743
|
}
|
|
2714
2744
|
else {
|
|
2715
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);
|
|
2716
2749
|
}
|
|
2717
2750
|
const base64Data = imageBuffer.toString("base64");
|
|
2718
2751
|
userContentParts.push({
|
|
@@ -2760,7 +2793,11 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
2760
2793
|
const legacyTool = tool;
|
|
2761
2794
|
const toolParams = legacyTool.parameters || tool.inputSchema;
|
|
2762
2795
|
if (toolParams) {
|
|
2763
|
-
|
|
2796
|
+
// Anthropic validates input_schema as JSON Schema draft 2020-12 and
|
|
2797
|
+
// rejects OpenAPI-3 dialect output (e.g. `nullable: true`) with a
|
|
2798
|
+
// 400 — use the default JSON Schema target, matching the direct
|
|
2799
|
+
// anthropic provider. The Gemini paths keep "openApi3".
|
|
2800
|
+
const jsonSchema = convertZodToJsonSchema(toolParams);
|
|
2764
2801
|
const inlined = inlineJsonSchema(jsonSchema);
|
|
2765
2802
|
anthropicTool.input_schema = {
|
|
2766
2803
|
type: "object",
|
|
@@ -2781,7 +2818,7 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
2781
2818
|
if (options.schema) {
|
|
2782
2819
|
useFinalResultTool = true;
|
|
2783
2820
|
// Convert schema to JSON schema format
|
|
2784
|
-
const schemaAsJson = convertZodToJsonSchema(options.schema
|
|
2821
|
+
const schemaAsJson = convertZodToJsonSchema(options.schema);
|
|
2785
2822
|
const inlinedSchema = inlineJsonSchema(schemaAsJson);
|
|
2786
2823
|
if (inlinedSchema.$schema) {
|
|
2787
2824
|
delete inlinedSchema.$schema;
|
|
@@ -4134,42 +4171,7 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
4134
4171
|
* Detect image MIME type from buffer
|
|
4135
4172
|
*/
|
|
4136
4173
|
detectImageType(buffer) {
|
|
4137
|
-
|
|
4138
|
-
if (buffer.length >= 8 &&
|
|
4139
|
-
buffer[0] === 0x89 &&
|
|
4140
|
-
buffer[1] === 0x50 &&
|
|
4141
|
-
buffer[2] === 0x4e &&
|
|
4142
|
-
buffer[3] === 0x47) {
|
|
4143
|
-
return "image/png";
|
|
4144
|
-
}
|
|
4145
|
-
// Check JPEG signature
|
|
4146
|
-
if (buffer.length >= 3 &&
|
|
4147
|
-
buffer[0] === 0xff &&
|
|
4148
|
-
buffer[1] === 0xd8 &&
|
|
4149
|
-
buffer[2] === 0xff) {
|
|
4150
|
-
return "image/jpeg";
|
|
4151
|
-
}
|
|
4152
|
-
// Check WebP signature
|
|
4153
|
-
if (buffer.length >= 12 &&
|
|
4154
|
-
buffer[0] === 0x52 &&
|
|
4155
|
-
buffer[1] === 0x49 &&
|
|
4156
|
-
buffer[2] === 0x46 &&
|
|
4157
|
-
buffer[3] === 0x46 &&
|
|
4158
|
-
buffer[8] === 0x57 &&
|
|
4159
|
-
buffer[9] === 0x45 &&
|
|
4160
|
-
buffer[10] === 0x42 &&
|
|
4161
|
-
buffer[11] === 0x50) {
|
|
4162
|
-
return "image/webp";
|
|
4163
|
-
}
|
|
4164
|
-
// Check GIF signature
|
|
4165
|
-
if (buffer.length >= 6 &&
|
|
4166
|
-
buffer[0] === 0x47 &&
|
|
4167
|
-
buffer[1] === 0x49 &&
|
|
4168
|
-
buffer[2] === 0x46) {
|
|
4169
|
-
return "image/gif";
|
|
4170
|
-
}
|
|
4171
|
-
// Default to PNG if unknown
|
|
4172
|
-
return "image/png";
|
|
4174
|
+
return detectImageMimeType(buffer);
|
|
4173
4175
|
}
|
|
4174
4176
|
/**
|
|
4175
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
|
+
}
|
|
@@ -18,3 +18,20 @@ export declare function hasRestrictedOutputLimit(modelName: string): boolean;
|
|
|
18
18
|
* Get the max output tokens for a model (32768 for restricted models)
|
|
19
19
|
*/
|
|
20
20
|
export declare const RESTRICTED_OUTPUT_TOKEN_LIMIT = 32768;
|
|
21
|
+
/**
|
|
22
|
+
* Normalize an Anthropic-API-style Claude model ID to the Vertex publisher
|
|
23
|
+
* format.
|
|
24
|
+
*
|
|
25
|
+
* The Anthropic API dates models with a trailing dash segment
|
|
26
|
+
* ("claude-haiku-4-5-20251001") while Vertex publisher IDs separate the date
|
|
27
|
+
* with "@" ("claude-haiku-4-5@20251001"). Vertex rejects the dash form with a
|
|
28
|
+
* 404 (verified live against us-east5), so the native Vertex+Claude paths
|
|
29
|
+
* normalize before calling @anthropic-ai/vertex-sdk.
|
|
30
|
+
*
|
|
31
|
+
* Pass-through cases: IDs already in "@" form, bare aliases with no date
|
|
32
|
+
* ("claude-sonnet-4-6" — Vertex resolves these itself), and non-Claude models.
|
|
33
|
+
* Legacy v2-suffixed Vertex IDs ("claude-3-5-sonnet-v2@20241022") have no
|
|
34
|
+
* dash-date equivalent, so those legacy dash IDs stay out of scope: they 404
|
|
35
|
+
* today and still 404 after the transform — no regression either way.
|
|
36
|
+
*/
|
|
37
|
+
export declare function toVertexAnthropicModelId(modelName: string): string;
|
|
@@ -105,3 +105,26 @@ export function hasRestrictedOutputLimit(modelName) {
|
|
|
105
105
|
* Get the max output tokens for a model (32768 for restricted models)
|
|
106
106
|
*/
|
|
107
107
|
export const RESTRICTED_OUTPUT_TOKEN_LIMIT = 32768;
|
|
108
|
+
/**
|
|
109
|
+
* Normalize an Anthropic-API-style Claude model ID to the Vertex publisher
|
|
110
|
+
* format.
|
|
111
|
+
*
|
|
112
|
+
* The Anthropic API dates models with a trailing dash segment
|
|
113
|
+
* ("claude-haiku-4-5-20251001") while Vertex publisher IDs separate the date
|
|
114
|
+
* with "@" ("claude-haiku-4-5@20251001"). Vertex rejects the dash form with a
|
|
115
|
+
* 404 (verified live against us-east5), so the native Vertex+Claude paths
|
|
116
|
+
* normalize before calling @anthropic-ai/vertex-sdk.
|
|
117
|
+
*
|
|
118
|
+
* Pass-through cases: IDs already in "@" form, bare aliases with no date
|
|
119
|
+
* ("claude-sonnet-4-6" — Vertex resolves these itself), and non-Claude models.
|
|
120
|
+
* Legacy v2-suffixed Vertex IDs ("claude-3-5-sonnet-v2@20241022") have no
|
|
121
|
+
* dash-date equivalent, so those legacy dash IDs stay out of scope: they 404
|
|
122
|
+
* today and still 404 after the transform — no regression either way.
|
|
123
|
+
*/
|
|
124
|
+
export function toVertexAnthropicModelId(modelName) {
|
|
125
|
+
if (!modelName.startsWith("claude-") || modelName.includes("@")) {
|
|
126
|
+
return modelName;
|
|
127
|
+
}
|
|
128
|
+
const dashDate = modelName.match(/^(claude-[a-z0-9-]+)-(\d{8})$/);
|
|
129
|
+
return dashDate ? `${dashDate[1]}@${dashDate[2]}` : modelName;
|
|
130
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juspay/neurolink",
|
|
3
|
-
"version": "9.70.
|
|
3
|
+
"version": "9.70.3",
|
|
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",
|