@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.
@@ -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
- const jsonSchema = convertZodToJsonSchema(toolParams, "openApi3");
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, "openApi3");
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
- const jsonSchema = convertZodToJsonSchema(toolParams, "openApi3");
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, "openApi3");
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
- // Check PNG signature
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
- const jsonSchema = convertZodToJsonSchema(toolParams, "openApi3");
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, "openApi3");
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
- const jsonSchema = convertZodToJsonSchema(toolParams, "openApi3");
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, "openApi3");
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
- // Check PNG signature
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.1",
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.27.4",
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.3",
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",