@juspay/neurolink 12.22.1 → 12.23.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.
@@ -0,0 +1,130 @@
1
+ {
2
+ "$schema": "./provider-catalog.schema.json",
3
+ "id": "friendli",
4
+ "displayName": "FriendliAI",
5
+ "aliases": [],
6
+ "tier": 2,
7
+ "wire": {
8
+ "baseURL": "https://api.friendli.ai/serverless/v1"
9
+ },
10
+ "models": {
11
+ "default": "zai-org/GLM-5.3",
12
+ "fallbacks": [
13
+ "zai-org/GLM-5.3",
14
+ "zai-org/GLM-5.3-Flash",
15
+ "zai-org/GLM-5.2",
16
+ "zai-org/GLM-5.1",
17
+ "google/gemma-4-31B-it",
18
+ "deepseek-ai/DeepSeek-V3.2",
19
+ "MiniMaxAI/MiniMax-M2.5"
20
+ ],
21
+ "defaultContextWindow": 1048576,
22
+ "defaultMaxOutputTokens": 131072,
23
+ "catalog": {
24
+ "zai-org/GLM-5.3": {
25
+ "contextWindow": 1048576,
26
+ "maxOutputTokens": 131072,
27
+ "pricingPerMTok": {
28
+ "input": 1.4,
29
+ "output": 4.4,
30
+ "cachedInput": 0.26
31
+ },
32
+ "vision": false,
33
+ "status": "production",
34
+ "description": "Recommended - Zhipu's flagship GLM-5.3, the only model in this roster that was live capability-probed end to end: a real structured tool call (finish_reason=tool_calls, 9 tool_calls deltas under streaming), SSE streaming (62 events, terminal [DONE]), and json_object structured output. A heavy internal reasoner exposed via reasoning/reasoning_content — at a small max_tokens budget the whole completion is spent on unfinished reasoning before any visible content, so give it a generous max_tokens. Rejects image/video input and rejects tools+response_format together, both HTTP 422 (see capabilities.structuredOutputWithTools). maxOutputTokens is the documented per-request output ceiling from https://friendli.ai/docs/examples/models/zai-glm-5-3/overview.md (retrieved 2026-09-09) — 131,072 tokens, distinct from the 1,048,576-token context window; the authenticated roster's max_completion_tokens field reports 1048576 (equal to context) but that is not treated as the output cap. Pricing per the same page: $1.40 / $4.40 per MTok input/output, $0.26 per MTok cached input."
35
+ },
36
+ "zai-org/GLM-5.3-Flash": {
37
+ "vision": false,
38
+ "status": "production",
39
+ "description": "Zhipu GLM-5.3 Flash — lighter/faster sibling of the default; present on the live roster (GET /serverless/v1/models, 200) but not itself capability-probed under the tight rate limit; fallback."
40
+ },
41
+ "zai-org/GLM-5.2": {
42
+ "vision": false,
43
+ "status": "production",
44
+ "description": "Zhipu GLM-5.2 — prior GLM generation; roster-listed only, not capability-probed; fallback."
45
+ },
46
+ "zai-org/GLM-5.1": {
47
+ "vision": false,
48
+ "status": "production",
49
+ "description": "Zhipu GLM-5.1 — prior GLM generation; roster-listed only, not capability-probed; fallback."
50
+ },
51
+ "google/gemma-4-31B-it": {
52
+ "vision": false,
53
+ "status": "production",
54
+ "description": "Google Gemma 4 31B instruct-tuned; roster-listed only, not capability-probed; fallback."
55
+ },
56
+ "deepseek-ai/DeepSeek-V3.2": {
57
+ "vision": false,
58
+ "status": "production",
59
+ "description": "DeepSeek V3.2; roster-listed only, not capability-probed; fallback."
60
+ },
61
+ "MiniMaxAI/MiniMax-M2.5": {
62
+ "vision": false,
63
+ "status": "production",
64
+ "description": "MiniMax M2.5; roster-listed only, not capability-probed; fallback."
65
+ }
66
+ },
67
+ "topModels": [
68
+ "zai-org/GLM-5.3",
69
+ "zai-org/GLM-5.3-Flash",
70
+ "zai-org/GLM-5.2",
71
+ "zai-org/GLM-5.1",
72
+ "google/gemma-4-31B-it",
73
+ "deepseek-ai/DeepSeek-V3.2",
74
+ "MiniMaxAI/MiniMax-M2.5"
75
+ ]
76
+ },
77
+ "capabilities": {
78
+ "text": true,
79
+ "streaming": true,
80
+ "tools": true,
81
+ "toolsWithStreaming": true,
82
+ "structuredOutput": true,
83
+ "structuredOutputWithTools": false,
84
+ "embeddings": false,
85
+ "thinking": false
86
+ },
87
+ "errorRules": [
88
+ {
89
+ "status": 401,
90
+ "pattern": "Unauthorized|valid credential",
91
+ "class": "authentication",
92
+ "message": "Invalid Friendli API key. Check {apiKeyEnvVar}. Get one at {setupUrl}"
93
+ },
94
+ {
95
+ "status": 404,
96
+ "pattern": "\\bModel\\s+['\"][^'\"\\r\\n]+['\"]\\s+not\\s+found\\b",
97
+ "class": "invalid-model",
98
+ "message": "Friendli model '{model}' not found. Pick a current model from the authenticated /serverless/v1/models roster or {setupUrl}"
99
+ }
100
+ ],
101
+ "setup": {
102
+ "url": "https://suite.friendli.ai/",
103
+ "apiKeyFormat": null,
104
+ "billingPolicy": "free-tier",
105
+ "instructions": [
106
+ "1. Visit: https://suite.friendli.ai/ and sign in",
107
+ "2. Create a Personal Access Token for the serverless endpoints",
108
+ "3. New accounts start with trial credit; confirm current billing terms in the console before heavy use",
109
+ "4. Set {apiKeyEnvVar} in your .env file"
110
+ ],
111
+ "description": "OpenAI-compatible endpoint at https://api.friendli.ai/serverless/v1, serving a small zai-org GLM-family roster plus a few third-party models. Rate limits are tight — pace requests at least 20 seconds apart."
112
+ },
113
+ "evidence": {
114
+ "rosterVerified": {
115
+ "date": "2026-09-06",
116
+ "method": "authenticated GET /serverless/v1/models",
117
+ "status": 200
118
+ },
119
+ "authProbe": {
120
+ "date": "2026-09-06",
121
+ "method": "POST /chat/completions with an invalid API key",
122
+ "status": 401
123
+ },
124
+ "liveMatrix": {
125
+ "date": "2026-09-06",
126
+ "result": "Probed against the default model, zai-org/GLM-5.3, via https://api.friendli.ai/serverless/v1. The first probe run used a 2s cadence, tripped Friendli's rate limit (12/16 cases 429) and is discarded entirely; this result is from a slower-paced re-run plus targeted single-call re-probes, each still hitting 429 occasionally even at ~25s spacing — Friendli's rate limits are tight, so any future live check needs generous pacing (20s+) and retry. Roster: GET /serverless/v1/models returns 200 with exactly 7 ids, all zai-org GLM family plus google/gemma-4-31B-it, deepseek-ai/DeepSeek-V3.2 and MiniMaxAI/MiniMax-M2.5 — models.fallbacks lists all 7. Wire mechanics on GLM-5.3: chat, system role, array content parts (message content IS accepted as a parts array, so no messageContentFormat quirk is declared), sampling params and max_completion_tokens all 200. Streaming: SSE, 200, 62 events, terminal [DONE] present — contentDeltas were 0 only because GLM-5.3 is a heavy reasoner that spent the small test budget entirely on reasoning/reasoning_content fields, not because streaming is broken. Tool calling: a real structured tool_call (get_time, tz argument parsed, finish_reason=tool_calls) on the first turn, a null-content assistant tool_calls turn accepted on replay, and 9 tool_calls deltas surfacing mid-SSE-stream — tools and toolsWithStreaming true; tool_choice is tolerated even with no tools declared. response_format json_object returns 200 cleanly — structuredOutput true. response_format json_schema also returns 200, but at the tested token budget the content is unfinished reasoning text rather than schema-conformant JSON — same reasoning-budget effect as above; NeuroLink's coerceJsonToSchema repair layer is the intended safety net. Sending tools and a JSON schema together in the same request is a genuine, hard rejection: HTTP 422 'The 'response_format' field cannot be set when tools are specified' — the same class of vendor exclusivity NeuroLink already handles generically for Groq via isToolsSchemaConflictError (pattern-matches the response_format+tool wording in the thrown error and retries the call without response_format), so no provider-specific quirk was added — structuredOutputWithTools is declared false. Vision: a real image part returns HTTP 422 'This model does not support image/video inputs.' — genuinely unsupported, not a rate limit; no model in this roster is marked vision:true and models.visionModel is left unset. Error shapes: bad key -> 401 {\"detail\":\"Unauthorized. Please provide a valid credential.\"}; bad model -> 404 {\"detail\":\"Model '<id>' not found.\"}. Only zai-org/GLM-5.3 (the declared default) was capability-probed end to end under the account's tight rate limit; the other 6 roster ids are roster-verified by id only, matching the account's usable request budget for this pass."
127
+ },
128
+ "addedInPR": "https://github.com/juspay/neurolink/pull/1657"
129
+ }
130
+ }
@@ -1,3 +1,3 @@
1
1
  import type { ProviderCatalogJson } from "../../types/index.js";
2
2
  export declare const CATALOG_JSON_ENTRIES: ProviderCatalogJson[];
3
- export declare const CATALOG_PROVIDER_IDS: readonly ["api-route", "baseten", "cerebras", "cloudflare", "fireworks", "gmicloud", "groq", "inception-labs", "io-intelligence", "mancer", "mistral", "perplexity", "sambanova", "together-ai", "upstage", "xai"];
3
+ export declare const CATALOG_PROVIDER_IDS: readonly ["api-route", "baseten", "cerebras", "cloudflare", "fireworks", "friendli", "gmicloud", "groq", "inception-labs", "io-intelligence", "mancer", "mistral", "perplexity", "sambanova", "together-ai", "upstage", "xai"];
@@ -5,6 +5,7 @@ import basetenJson from "./baseten.json" with { type: "json" };
5
5
  import cerebrasJson from "./cerebras.json" with { type: "json" };
6
6
  import cloudflareJson from "./cloudflare.json" with { type: "json" };
7
7
  import fireworksJson from "./fireworks.json" with { type: "json" };
8
+ import friendliJson from "./friendli.json" with { type: "json" };
8
9
  import gmicloudJson from "./gmicloud.json" with { type: "json" };
9
10
  import groqJson from "./groq.json" with { type: "json" };
10
11
  import inceptionLabsJson from "./inception-labs.json" with { type: "json" };
@@ -22,6 +23,7 @@ export const CATALOG_JSON_ENTRIES = [
22
23
  cerebrasJson,
23
24
  cloudflareJson,
24
25
  fireworksJson,
26
+ friendliJson,
25
27
  gmicloudJson,
26
28
  groqJson,
27
29
  inceptionLabsJson,
@@ -40,6 +42,7 @@ export const CATALOG_PROVIDER_IDS = [
40
42
  "cerebras",
41
43
  "cloudflare",
42
44
  "fireworks",
45
+ "friendli",
43
46
  "gmicloud",
44
47
  "groq",
45
48
  "inception-labs",
@@ -9,9 +9,8 @@
9
9
  },
10
10
  "models": {
11
11
  "default": "mistral-small-2506",
12
- "fallbackModelName": "mistral-small-2506",
13
12
  "registryDefaultModel": "mistral-large-latest",
14
- "fallbacks": ["mistral-small-2506", "mistral-large-latest"],
13
+ "fallbacks": ["mistral-small-2506", "mistral-medium-latest"],
15
14
  "defaultContextWindow": 128000,
16
15
  "defaultMaxOutputTokens": 4096,
17
16
  "catalog": {
@@ -230,6 +230,40 @@ const catalogModelsSchema = z
230
230
  message: "models.fallbackModelName must be a key of models.catalog",
231
231
  });
232
232
  }
233
+ // A fallback that resolves to the default model cannot fall back.
234
+ //
235
+ // `loader.ts` derives it positionally as
236
+ // `fallbackModelName ?? fallbacks[1] ?? fallbacks[0]`, and nothing checked
237
+ // the result. A provider with one model has no choice and is left alone;
238
+ // a provider with alternatives that still lands on its own default has
239
+ // silently disabled its retry path — the primary fails, the SDK retries
240
+ // the same model, and it fails again for the same reason.
241
+ //
242
+ // This is not hypothetical. `mistral.json` ships 32 models and explicitly
243
+ // sets `fallbackModelName` to its own default, overriding a perfectly good
244
+ // `fallbacks[1]`. Two single-model catalogs also resolve this way and are
245
+ // correctly exempt.
246
+ const derivedFallback = models.fallbackModelName ?? models.fallbacks[1] ?? models.fallbacks[0];
247
+ // Count only entries that could actually serve as a fallback. The whole
248
+ // `catalog` map also carries retired ids, so its size both over-reports in
249
+ // the message below and could fire this rule on a catalog whose only other
250
+ // entries are unusable — the mirror of the single-model exemption.
251
+ const selectableModels = Object.values(models.catalog).filter((spec) => spec.status !== "retired").length;
252
+ if (derivedFallback !== undefined &&
253
+ derivedFallback === models.default &&
254
+ selectableModels > 1) {
255
+ ctx.addIssue({
256
+ code: "custom",
257
+ path: models.fallbackModelName !== undefined
258
+ ? ["fallbackModelName"]
259
+ : ["fallbacks"],
260
+ message: `the fallback model resolves to "${derivedFallback}", which is also models.default — ` +
261
+ `a fallback identical to the primary cannot fall back. This catalog declares ` +
262
+ `${selectableModels} selectable models, so name a different one (or drop ` +
263
+ `fallbackModelName to ` +
264
+ `let fallbacks[1] apply). Single-model catalogs are exempt.`,
265
+ });
266
+ }
233
267
  if (models.registryDefaultModel !== undefined &&
234
268
  !catalogKeys.has(models.registryDefaultModel)) {
235
269
  ctx.addIssue({
@@ -380,7 +380,13 @@ export class GoogleAIStudioProvider extends BaseProvider {
380
380
  if ("inlineData" in part && part.inlineData?.data) {
381
381
  const foundImageData = part.inlineData.data;
382
382
  imageData = foundImageData;
383
- const mimeType = part.inlineData.mimeType || "image/png";
383
+ // Keep the DECLARED value separate from the display default.
384
+ // ImageGenService trusts a string `mimeType` on imageOutput and
385
+ // skips sniffing, so forwarding a hardcoded "image/png" for a
386
+ // vendor that declared nothing would defeat the byte detection
387
+ // — WebP bytes would ship labelled PNG, with a .png extension.
388
+ const declaredMimeType = part.inlineData.mimeType;
389
+ const mimeType = declaredMimeType || "image/png";
384
390
  logger.info("Image generation successful", {
385
391
  model: imageModelName,
386
392
  mimeType,
@@ -390,7 +396,12 @@ export class GoogleAIStudioProvider extends BaseProvider {
390
396
  const result = {
391
397
  content: `Generated image using ${imageModelName} (${mimeType})`,
392
398
  imageOutput: {
399
+ // The vendor declares the format on the inline part; that
400
+ // beats sniffing it back out of the bytes. When it declares
401
+ // nothing, omit the field entirely so the service sniffs
402
+ // rather than trusting a default we invented.
393
403
  base64: foundImageData,
404
+ ...(declaredMimeType ? { mimeType: declaredMimeType } : {}),
394
405
  },
395
406
  provider: this.providerName,
396
407
  model: imageModelName,
@@ -434,7 +445,10 @@ export class GoogleAIStudioProvider extends BaseProvider {
434
445
  if ("inlineData" in part && part.inlineData?.data) {
435
446
  const foundImageData = part.inlineData.data;
436
447
  imageData = foundImageData;
437
- const mimeType = part.inlineData.mimeType || "image/png";
448
+ // See the streaming site above: only a mime type the vendor
449
+ // actually declared may be forwarded, or the sniffer is bypassed.
450
+ const declaredMimeType = part.inlineData.mimeType;
451
+ const mimeType = declaredMimeType || "image/png";
438
452
  logger.info("Image generation successful (non-streaming)", {
439
453
  model: imageModelName,
440
454
  mimeType,
@@ -444,7 +458,9 @@ export class GoogleAIStudioProvider extends BaseProvider {
444
458
  const result = {
445
459
  content: `Generated image using ${imageModelName} (${mimeType})`,
446
460
  imageOutput: {
461
+ // As above: forward only a declared value, never a default.
447
462
  base64: foundImageData,
463
+ ...(declaredMimeType ? { mimeType: declaredMimeType } : {}),
448
464
  },
449
465
  provider: this.providerName,
450
466
  model: imageModelName,
@@ -5229,7 +5229,12 @@ export class GoogleVertexProvider extends BaseProvider {
5229
5229
  if (imageOutput) {
5230
5230
  yield {
5231
5231
  type: "image",
5232
- imageOutput: { base64: imageOutput.base64 || "" },
5232
+ imageOutput: {
5233
+ base64: imageOutput.base64 || "",
5234
+ ...(imageOutput.mimeType
5235
+ ? { mimeType: imageOutput.mimeType }
5236
+ : {}),
5237
+ },
5233
5238
  };
5234
5239
  }
5235
5240
  yield { content: textContent };
@@ -6370,9 +6375,12 @@ export class GoogleVertexProvider extends BaseProvider {
6370
6375
  }
6371
6376
  // Extract image data (handle both formats)
6372
6377
  const imageData = imagePart.inlineData?.data || imagePart.inline_data?.data;
6373
- const mimeType = imagePart.inlineData?.mimeType ||
6374
- imagePart.inline_data?.mime_type ||
6375
- "image/png";
6378
+ // The declared label and the display default are deliberately separate.
6379
+ // `ImageGenService` trusts a string `mimeType` and skips sniffing, so
6380
+ // stamping the `image/png` default onto `imageOutput` would relabel an
6381
+ // unlabelled WebP as PNG — the exact defect this change fixes elsewhere.
6382
+ const declaredMimeType = imagePart.inlineData?.mimeType || imagePart.inline_data?.mime_type;
6383
+ const mimeType = declaredMimeType || "image/png";
6376
6384
  if (!imageData) {
6377
6385
  throw new ProviderError("Image part found but no data available", this.providerName);
6378
6386
  }
@@ -6387,6 +6395,7 @@ export class GoogleVertexProvider extends BaseProvider {
6387
6395
  content: `Generated image using ${imageModelName} (${mimeType})`,
6388
6396
  imageOutput: {
6389
6397
  base64: imageData,
6398
+ ...(declaredMimeType ? { mimeType: declaredMimeType } : {}),
6390
6399
  },
6391
6400
  provider: this.providerName,
6392
6401
  model: imageModelName,
@@ -7,6 +7,7 @@ import { logger } from "../utils/logger.js";
7
7
  import { createRecraftConfig, getProviderModel, validateApiKey, } from "../utils/providerConfig.js";
8
8
  import { MAX_IMAGE_BYTES, readBoundedBuffer } from "../utils/sizeGuard.js";
9
9
  import { assertSafeUrl } from "../utils/ssrfGuard.js";
10
+ import { detectImageMimeType } from "../utils/imageDetection.js";
10
11
  const RECRAFT_DEFAULT_BASE_URL = "https://external.api.recraft.ai/v1";
11
12
  const REQUEST_TIMEOUT_MS = 120_000;
12
13
  const getRecraftApiKey = () => validateApiKey(createRecraftConfig());
@@ -178,7 +179,13 @@ export class RecraftProvider extends BaseProvider {
178
179
  model: this.modelName,
179
180
  // output: 1000 = sentinel for per-image pricing (see pricing.ts)
180
181
  usage: { input: 0, output: 1000, total: 1000 },
181
- imageOutput: { base64 },
182
+ // Recraft answers b64_json with WebP bytes for V3 raster models; label
183
+ // the real format from the leading bytes instead of leaving callers to
184
+ // assume PNG.
185
+ imageOutput: {
186
+ base64,
187
+ mimeType: detectImageMimeType(Buffer.from(base64.slice(0, 64), "base64")),
188
+ },
182
189
  };
183
190
  }
184
191
  async validateConfiguration() {
@@ -405,6 +405,8 @@ export type CliGenerateResult = CommandResult & {
405
405
  imageOutput?: {
406
406
  base64: string;
407
407
  savedPath?: string;
408
+ /** Encoded format, when the provider identified it (e.g. Recraft returns WebP). */
409
+ mimeType?: string;
408
410
  } | null;
409
411
  };
410
412
  /**
@@ -10,7 +10,7 @@ import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
10
10
  /**
11
11
  * Supported MCP transport protocols - imported from mcpTypes.js (canonical definition)
12
12
  */
13
- import type { MCPTransportType, MCPServerInfo } from "./mcp.js";
13
+ import type { MCPTransportType, MCPServerInfo, MCPServerReadiness } from "./mcp.js";
14
14
  export type { MCPTransportType } from "./mcp.js";
15
15
  /**
16
16
  * External MCP server configuration for process spawning
@@ -182,10 +182,28 @@ export type ExternalMCPOperationResult<T = unknown> = {
182
182
  serverId?: string;
183
183
  /** Operation duration in milliseconds */
184
184
  duration?: number;
185
- /** Additional metadata */
185
+ /**
186
+ * Additional metadata.
187
+ *
188
+ * The two typed optional fields are intersected with the pre-existing
189
+ * `JsonValue` index signature rather than declared inside it: declaring an
190
+ * optional property next to a `JsonValue` index signature does not compile
191
+ * (its type includes `undefined`), and widening the index signature to
192
+ * `JsonValue | undefined` would change the read type of every other key
193
+ * for every existing consumer — a backward-incompatible public change.
194
+ */
186
195
  metadata?: {
187
196
  timestamp: number;
188
197
  operation: string;
198
+ /**
199
+ * Set on `addServer` results: whether the registration discovered
200
+ * enough tools to be considered ready (see `MCPServerInfo.minTools`).
201
+ * Absent on operations the minTools gate does not apply to.
202
+ */
203
+ readiness?: MCPServerReadiness;
204
+ /** Set alongside `readiness` on `addServer` results — tools discovered, post block-list. */
205
+ toolsDiscovered?: number;
206
+ } & {
189
207
  [key: string]: JsonValue;
190
208
  };
191
209
  };
@@ -954,9 +954,14 @@ export type MediaGenerationOutputs = {
954
954
  * ```
955
955
  */
956
956
  ppt?: PPTGenerationResult;
957
- /** Standard format for image generation */
957
+ /**
958
+ * Standard format for image generation. `mimeType` is set when the provider
959
+ * can identify the encoded format (sniffed from the image bytes, e.g.
960
+ * Recraft returns WebP), so callers do not have to assume PNG.
961
+ */
958
962
  imageOutput?: {
959
963
  base64: string;
964
+ mimeType?: string;
960
965
  } | null;
961
966
  /** STT transcription result (present when stt.enabled is true and audio input was provided) */
962
967
  transcription?: STTResult;
@@ -17,6 +17,13 @@ export type MCPTransportType = "stdio" | "sse" | "websocket" | "http" | "ws" | "
17
17
  * MCP Server Connection Status - Individual server status
18
18
  */
19
19
  export type MCPServerConnectionStatus = "initializing" | "connecting" | "connected" | "disconnected" | "failed" | "restarting" | "stopping" | "stopped";
20
+ /**
21
+ * Readiness of an external MCP server registration, distinct from raw
22
+ * connection status: a server can be "connected" at the transport level yet
23
+ * still be gated as not ready when it discovers fewer tools than its
24
+ * configured `minTools` floor.
25
+ */
26
+ export type MCPServerReadiness = "ready" | "insufficient_tools" | "failed";
20
27
  /**
21
28
  * MCP Server Category Types - Deployment and server type classification
22
29
  */
@@ -56,6 +63,16 @@ export type MCPServerInfo = {
56
63
  cwd?: string;
57
64
  autoRestart?: boolean;
58
65
  healthCheckInterval?: number;
66
+ /**
67
+ * Minimum number of tools that must be discovered for this server's
68
+ * registration to be considered ready (default: 0 — no minimum, so a
69
+ * resource/prompt-only server that legitimately exposes zero tools still
70
+ * registers successfully). When discovery finds fewer tools than this
71
+ * floor, `ExternalServerManager.addServer` returns `success: false` with
72
+ * `metadata.readiness: "insufficient_tools"` instead of marking the
73
+ * server connected/healthy, and tears the connection back down.
74
+ */
75
+ minTools?: number;
59
76
  /** Retry configuration for HTTP transport */
60
77
  retryConfig?: {
61
78
  maxAttempts?: number;
@@ -1,2 +1,2 @@
1
- export type CatalogProviderName = "api-route" | "baseten" | "cerebras" | "cloudflare" | "fireworks" | "gmicloud" | "groq" | "inception-labs" | "io-intelligence" | "mancer" | "mistral" | "perplexity" | "sambanova" | "together-ai" | "upstage" | "xai";
2
- export type CatalogCredentialKey = "apiRoute" | "baseten" | "cerebras" | "cloudflare" | "fireworks" | "gmicloud" | "groq" | "inceptionLabs" | "ioIntelligence" | "mancer" | "mistral" | "perplexity" | "sambanova" | "together" | "upstage" | "xai";
1
+ export type CatalogProviderName = "api-route" | "baseten" | "cerebras" | "cloudflare" | "fireworks" | "friendli" | "gmicloud" | "groq" | "inception-labs" | "io-intelligence" | "mancer" | "mistral" | "perplexity" | "sambanova" | "together-ai" | "upstage" | "xai";
2
+ export type CatalogCredentialKey = "apiRoute" | "baseten" | "cerebras" | "cloudflare" | "fireworks" | "friendli" | "gmicloud" | "groq" | "inceptionLabs" | "ioIntelligence" | "mancer" | "mistral" | "perplexity" | "sambanova" | "together" | "upstage" | "xai";
@@ -200,6 +200,10 @@ export type NeurolinkCredentials = {
200
200
  apiKey?: string;
201
201
  baseURL?: string;
202
202
  };
203
+ friendli?: {
204
+ apiKey?: string;
205
+ baseURL?: string;
206
+ };
203
207
  gmicloud?: {
204
208
  apiKey?: string;
205
209
  baseURL?: string;
@@ -7,6 +7,16 @@
7
7
  * otherwise a wrong default (historically `image/jpeg`) makes Anthropic reject
8
8
  * PNG/GIF/WebP with a media-type mismatch 400.
9
9
  */
10
+ /**
11
+ * Detect an image's MIME type from its magic bytes, or `null` when the bytes
12
+ * match no known signature.
13
+ *
14
+ * Use this when the answer "not an image" has to be distinguishable from a
15
+ * default. `detectImageMimeType` collapses that distinction on purpose, which
16
+ * is right for a caller that already knows it holds an image and wrong for one
17
+ * deciding whether it does.
18
+ */
19
+ export declare function sniffImageMimeType(buffer: Buffer): string | null;
10
20
  /**
11
21
  * Detect an image's MIME type from its magic bytes. Returns `image/png` for
12
22
  * buffers that match no known signature (the safest neutral default for the
@@ -8,17 +8,27 @@
8
8
  * PNG/GIF/WebP with a media-type mismatch 400.
9
9
  */
10
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).
11
+ * Detect an image's MIME type from its magic bytes, or `null` when the bytes
12
+ * match no known signature.
13
+ *
14
+ * Use this when the answer "not an image" has to be distinguishable from a
15
+ * default. `detectImageMimeType` collapses that distinction on purpose, which
16
+ * is right for a caller that already knows it holds an image and wrong for one
17
+ * deciding whether it does.
14
18
  */
15
- export function detectImageMimeType(buffer) {
16
- // PNG: 89 50 4E 47
19
+ export function sniffImageMimeType(buffer) {
20
+ // PNG: the full 8-byte signature 89 50 4E 47 0D 0A 1A 0A. The trailing four
21
+ // bytes matter — a truncated `iVBORw==` carries only the first four and is
22
+ // not a decodable image.
17
23
  if (buffer.length >= 8 &&
18
24
  buffer[0] === 0x89 &&
19
25
  buffer[1] === 0x50 &&
20
26
  buffer[2] === 0x4e &&
21
- buffer[3] === 0x47) {
27
+ buffer[3] === 0x47 &&
28
+ buffer[4] === 0x0d &&
29
+ buffer[5] === 0x0a &&
30
+ buffer[6] === 0x1a &&
31
+ buffer[7] === 0x0a) {
22
32
  return "image/png";
23
33
  }
24
34
  // JPEG: FF D8 FF
@@ -40,13 +50,29 @@ export function detectImageMimeType(buffer) {
40
50
  buffer[11] === 0x50) {
41
51
  return "image/webp";
42
52
  }
43
- // GIF: "GIF"
53
+ // GIF: the full 6-byte header "GIF87a" or "GIF89a". The version stamp is
54
+ // what separates a header from three coincidental letters — the old check
55
+ // compared only "GIF" while guarding on `length >= 6`, so any six bytes
56
+ // beginning with those letters (base64 `R0lGAAAA` decodes to 47 49 46 00
57
+ // 00 00) were labelled image/gif. This gate is what decides whether bytes
58
+ // are an image at all, so it is only as strict as its weakest signature.
44
59
  if (buffer.length >= 6 &&
45
- buffer[0] === 0x47 &&
46
- buffer[1] === 0x49 &&
47
- buffer[2] === 0x46) {
60
+ buffer[0] === 0x47 && // G
61
+ buffer[1] === 0x49 && // I
62
+ buffer[2] === 0x46 && // F
63
+ buffer[3] === 0x38 && // 8
64
+ (buffer[4] === 0x37 || buffer[4] === 0x39) && // 7 | 9
65
+ buffer[5] === 0x61 // a
66
+ ) {
48
67
  return "image/gif";
49
68
  }
50
- // Unknown — neutral default.
51
- return "image/png";
69
+ return null;
70
+ }
71
+ /**
72
+ * Detect an image's MIME type from its magic bytes. Returns `image/png` for
73
+ * buffers that match no known signature (the safest neutral default for the
74
+ * Vertex image path).
75
+ */
76
+ export function detectImageMimeType(buffer) {
77
+ return sniffImageMimeType(buffer) ?? "image/png";
52
78
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/neurolink",
3
- "version": "12.22.1",
3
+ "version": "12.23.0",
4
4
  "packageManager": "pnpm@10.15.1",
5
5
  "description": "The pipe layer of an AI nervous system: one interface connecting provider neurons to your application, across three inference types — generate, stream and decide. `decide` returns typed, calibrated judgements from a non-generative model (~400ms, ~$0.00002/call) for routing, tool selection and context budgeting. MCP-native (4 transports), voice TTS/STT/realtime, RAG, agents, memory, compaction, 9 observability exporters. OpenAI · Anthropic · Gemini · Bedrock · Azure · Ollama · TypeSafe Jev and more.",
6
6
  "author": {
@@ -89,7 +89,8 @@
89
89
  "test:mcp:cli": "pnpm exec tsx test/continuous-test-suite-mcp-cli.ts",
90
90
  "test:mcp:stdio-lifecycle": "pnpm exec tsx test/continuous-test-suite-mcp-stdio-lifecycle.ts",
91
91
  "test:mcp-direct-name-repair": "pnpm exec tsx test/continuous-test-suite-mcp-direct-name-repair.ts",
92
- "test:mcp:full": "pnpm run test:mcp:infra && pnpm run test:mcp:spans && pnpm run test:mcp:stdio-lifecycle && pnpm run test:mcp:sdk && pnpm run test:mcp:cli && pnpm run test:mcp:http",
92
+ "test:mcp:min-tools-readiness": "pnpm exec tsx test/continuous-test-suite-mcp-min-tools-readiness.ts",
93
+ "test:mcp:full": "pnpm run test:mcp:infra && pnpm run test:mcp:spans && pnpm run test:mcp:stdio-lifecycle && pnpm run test:mcp:min-tools-readiness && pnpm run test:mcp:sdk && pnpm run test:mcp:cli && pnpm run test:mcp:http",
93
94
  "test:media": "pnpm exec tsx test/continuous-test-suite-media-gen.ts",
94
95
  "test:media-registry-collisions": "pnpm exec tsx test/continuous-test-suite-media-registry-collisions.ts",
95
96
  "test:memory": "pnpm exec tsx test/continuous-test-suite-memory.ts",