@juspay/neurolink 12.22.0 → 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.
- package/CHANGELOG.md +6 -3
- package/dist/adapters/providerImageAdapter.js +16 -11
- package/dist/browser/neurolink.min.js +379 -379
- package/dist/cli/factories/commandFactory.js +26 -3
- package/dist/constants/enums.d.ts +10 -0
- package/dist/constants/enums.js +11 -0
- package/dist/core/constants.js +1 -1
- package/dist/factories/providerDescriptors.d.ts +1 -1
- package/dist/factories/providerDescriptors.js +15 -13
- package/dist/image-gen/ImageGenService.js +16 -14
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/mcp/externalServerManager.d.ts +9 -0
- package/dist/mcp/externalServerManager.js +88 -16
- package/dist/neurolink.js +18 -11
- package/dist/providers/amazonBedrock/client.js +30 -1
- package/dist/providers/catalog/friendli.json +130 -0
- package/dist/providers/catalog/index.generated.d.ts +1 -1
- package/dist/providers/catalog/index.generated.js +3 -0
- package/dist/providers/catalog/mistral.json +1 -2
- package/dist/providers/catalog/schema.js +34 -0
- package/dist/providers/googleAiStudio/client.js +18 -2
- package/dist/providers/googleVertex/client.js +13 -4
- package/dist/providers/openaiChatCompletionsClient.d.ts +4 -0
- package/dist/providers/openaiChatCompletionsClient.js +20 -5
- package/dist/providers/recraft.js +8 -1
- package/dist/types/cli.d.ts +2 -0
- package/dist/types/externalMcp.d.ts +20 -2
- package/dist/types/file.d.ts +13 -0
- package/dist/types/generate.d.ts +6 -1
- package/dist/types/mcp.d.ts +17 -0
- package/dist/types/providerCatalog.generated.d.ts +2 -2
- package/dist/types/providers.d.ts +4 -0
- package/dist/utils/imageDetection.d.ts +10 -0
- package/dist/utils/imageDetection.js +38 -12
- package/dist/utils/pdfProcessor.d.ts +21 -3
- package/dist/utils/pdfProcessor.js +124 -48
- package/docs-site/static/search-index.json +1 -1
- package/package.json +3 -4
|
@@ -10,6 +10,7 @@ import { ContextFactory, } from "../../types/index.js";
|
|
|
10
10
|
import { checkRedisAvailability } from "../../utils/conversationMemory.js";
|
|
11
11
|
import { normalizeEvaluationData } from "../../utils/evaluationUtils.js";
|
|
12
12
|
import { logger } from "../../utils/logger.js";
|
|
13
|
+
import { sniffImageMimeType } from "../../utils/imageDetection.js";
|
|
13
14
|
import { createThinkingConfigFromRecord } from "../../utils/thinkingConfig.js";
|
|
14
15
|
import { buildToolRoutingConfigFromCli } from "../utils/toolRoutingFlags.js";
|
|
15
16
|
import { buildClassifierRouterConfigFromCli } from "../utils/classifierRouterFlags.js";
|
|
@@ -233,7 +234,7 @@ export class CLICommandFactory {
|
|
|
233
234
|
},
|
|
234
235
|
imageOutput: {
|
|
235
236
|
type: "string",
|
|
236
|
-
description: "Custom path for generated image (default: generated-images/image-<timestamp
|
|
237
|
+
description: "Custom path for generated image (default: generated-images/image-<timestamp>.<ext>, where the extension follows the format the provider actually returned)",
|
|
237
238
|
alias: "image-output",
|
|
238
239
|
},
|
|
239
240
|
// Behavior control options
|
|
@@ -1060,7 +1061,8 @@ export class CLICommandFactory {
|
|
|
1060
1061
|
else {
|
|
1061
1062
|
const imageDir = "generated-images";
|
|
1062
1063
|
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
1063
|
-
|
|
1064
|
+
const ext = imageExtensionFor(generateResult.imageOutput.mimeType, generateResult.imageOutput.base64);
|
|
1065
|
+
imagePath = path.join(imageDir, `image-${timestamp}.${ext}`);
|
|
1064
1066
|
// Create directory if it doesn't exist
|
|
1065
1067
|
if (!fs.existsSync(imageDir)) {
|
|
1066
1068
|
fs.mkdirSync(imageDir, { recursive: true });
|
|
@@ -3055,7 +3057,8 @@ export class CLICommandFactory {
|
|
|
3055
3057
|
else {
|
|
3056
3058
|
const imageDir = "generated-images";
|
|
3057
3059
|
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
3058
|
-
|
|
3060
|
+
const ext = imageExtensionFor(undefined, streamResult.imageBase64);
|
|
3061
|
+
imagePath = path.join(imageDir, `image-${timestamp}.${ext}`);
|
|
3059
3062
|
if (!fs.existsSync(imageDir)) {
|
|
3060
3063
|
fs.mkdirSync(imageDir, { recursive: true });
|
|
3061
3064
|
}
|
|
@@ -4545,4 +4548,24 @@ export class CLICommandFactory {
|
|
|
4545
4548
|
}
|
|
4546
4549
|
/** Re-export of CLICommandFactory's static option definitions for direct import by tests/tooling. */
|
|
4547
4550
|
export const commonOptions = CLICommandFactory.commonOptions;
|
|
4551
|
+
/**
|
|
4552
|
+
* File extension for generated image bytes. The provider now labels what it
|
|
4553
|
+
* returned, so a WebP no longer gets written to a `.png` filename — which is
|
|
4554
|
+
* the same wrong-label problem one layer up from the provider.
|
|
4555
|
+
*/
|
|
4556
|
+
function imageExtensionFor(mimeType, base64) {
|
|
4557
|
+
const known = {
|
|
4558
|
+
"image/png": "png",
|
|
4559
|
+
"image/jpeg": "jpg",
|
|
4560
|
+
"image/webp": "webp",
|
|
4561
|
+
"image/gif": "gif",
|
|
4562
|
+
};
|
|
4563
|
+
if (mimeType && known[mimeType]) {
|
|
4564
|
+
return known[mimeType];
|
|
4565
|
+
}
|
|
4566
|
+
// No label from the provider: sniff the leading bytes rather than assume.
|
|
4567
|
+
const head = Buffer.from(base64.slice(0, 64), "base64");
|
|
4568
|
+
const sniffed = sniffImageMimeType(head);
|
|
4569
|
+
return sniffed ? known[sniffed] : "png";
|
|
4570
|
+
}
|
|
4548
4571
|
//# sourceMappingURL=commandFactory.js.map
|
|
@@ -23,6 +23,7 @@ export declare enum AIProviderName {
|
|
|
23
23
|
CEREBRAS = "cerebras",
|
|
24
24
|
CLOUDFLARE = "cloudflare",
|
|
25
25
|
FIREWORKS = "fireworks",
|
|
26
|
+
FRIENDLI = "friendli",
|
|
26
27
|
GMICLOUD = "gmicloud",
|
|
27
28
|
GROQ = "groq",
|
|
28
29
|
INCEPTION_LABS = "inception-labs",
|
|
@@ -876,6 +877,15 @@ export declare enum FireworksModels {
|
|
|
876
877
|
LLAMA_V3P2_11B_VISION_INSTRUCT = "accounts/fireworks/models/llama-v3p2-11b-vision-instruct",
|
|
877
878
|
PHI_3_VISION_128K_INSTRUCT = "accounts/fireworks/models/phi-3-vision-128k-instruct"
|
|
878
879
|
}
|
|
880
|
+
export declare enum FriendliModels {
|
|
881
|
+
ZAI_ORG_GLM_5_3 = "zai-org/GLM-5.3",
|
|
882
|
+
ZAI_ORG_GLM_5_3_FLASH = "zai-org/GLM-5.3-Flash",
|
|
883
|
+
ZAI_ORG_GLM_5_2 = "zai-org/GLM-5.2",
|
|
884
|
+
ZAI_ORG_GLM_5_1 = "zai-org/GLM-5.1",
|
|
885
|
+
GOOGLE_GEMMA_4_31B_IT = "google/gemma-4-31B-it",
|
|
886
|
+
DEEPSEEK_AI_DEEPSEEK_V3_2 = "deepseek-ai/DeepSeek-V3.2",
|
|
887
|
+
MINIMAXAI_MINIMAX_M2_5 = "MiniMaxAI/MiniMax-M2.5"
|
|
888
|
+
}
|
|
879
889
|
export declare enum GmicloudModels {
|
|
880
890
|
MINIMAXAI_MINIMAX_M3 = "MiniMaxAI/MiniMax-M3"
|
|
881
891
|
}
|
package/dist/constants/enums.js
CHANGED
|
@@ -28,6 +28,7 @@ export var AIProviderName;
|
|
|
28
28
|
AIProviderName["CEREBRAS"] = "cerebras";
|
|
29
29
|
AIProviderName["CLOUDFLARE"] = "cloudflare";
|
|
30
30
|
AIProviderName["FIREWORKS"] = "fireworks";
|
|
31
|
+
AIProviderName["FRIENDLI"] = "friendli";
|
|
31
32
|
AIProviderName["GMICLOUD"] = "gmicloud";
|
|
32
33
|
AIProviderName["GROQ"] = "groq";
|
|
33
34
|
AIProviderName["INCEPTION_LABS"] = "inception-labs";
|
|
@@ -1135,6 +1136,16 @@ export var FireworksModels;
|
|
|
1135
1136
|
FireworksModels["LLAMA_V3P2_11B_VISION_INSTRUCT"] = "accounts/fireworks/models/llama-v3p2-11b-vision-instruct";
|
|
1136
1137
|
FireworksModels["PHI_3_VISION_128K_INSTRUCT"] = "accounts/fireworks/models/phi-3-vision-128k-instruct";
|
|
1137
1138
|
})(FireworksModels || (FireworksModels = {}));
|
|
1139
|
+
export var FriendliModels;
|
|
1140
|
+
(function (FriendliModels) {
|
|
1141
|
+
FriendliModels["ZAI_ORG_GLM_5_3"] = "zai-org/GLM-5.3";
|
|
1142
|
+
FriendliModels["ZAI_ORG_GLM_5_3_FLASH"] = "zai-org/GLM-5.3-Flash";
|
|
1143
|
+
FriendliModels["ZAI_ORG_GLM_5_2"] = "zai-org/GLM-5.2";
|
|
1144
|
+
FriendliModels["ZAI_ORG_GLM_5_1"] = "zai-org/GLM-5.1";
|
|
1145
|
+
FriendliModels["GOOGLE_GEMMA_4_31B_IT"] = "google/gemma-4-31B-it";
|
|
1146
|
+
FriendliModels["DEEPSEEK_AI_DEEPSEEK_V3_2"] = "deepseek-ai/DeepSeek-V3.2";
|
|
1147
|
+
FriendliModels["MINIMAXAI_MINIMAX_M2_5"] = "MiniMaxAI/MiniMax-M2.5";
|
|
1148
|
+
})(FriendliModels || (FriendliModels = {}));
|
|
1138
1149
|
export var GmicloudModels;
|
|
1139
1150
|
(function (GmicloudModels) {
|
|
1140
1151
|
GmicloudModels["MINIMAXAI_MINIMAX_M3"] = "MiniMaxAI/MiniMax-M3";
|
package/dist/core/constants.js
CHANGED
|
@@ -311,7 +311,7 @@ export const PDF_LIMITS = {
|
|
|
311
311
|
DEFAULT_MAX_CANVAS_PIXELS: 16_777_216,
|
|
312
312
|
// Floor for the downscaled render scale (#260 follow-up). A crafted/malformed
|
|
313
313
|
// MediaBox can drive the estimated pixel count toward Infinity, which would
|
|
314
|
-
// otherwise collapse `effectiveScale` to 0 and hand
|
|
314
|
+
// otherwise collapse `effectiveScale` to 0 and hand the renderer a degenerate
|
|
315
315
|
// viewport. Never let the downscale branch go below this.
|
|
316
316
|
MIN_EFFECTIVE_SCALE: 0.1,
|
|
317
317
|
};
|
|
@@ -3,7 +3,7 @@ import type { ProviderDescriptor } from "../types/index.js";
|
|
|
3
3
|
/**
|
|
4
4
|
* Single source of truth for provider identity, credentials, defaults, and
|
|
5
5
|
* runtime behavior classification — the hand-maintained providers plus the
|
|
6
|
-
*
|
|
6
|
+
* 15 JSON-catalog providers derived above.
|
|
7
7
|
*/
|
|
8
8
|
export declare const PROVIDER_DESCRIPTORS: readonly ProviderDescriptor[];
|
|
9
9
|
/** O(1) canonical-name → descriptor lookup. */
|
|
@@ -5,10 +5,10 @@ import { getCatalogJsonEntries, catalogCredentialsKey, catalogEnvVar, } from "..
|
|
|
5
5
|
import { DEFAULT_INFERENCE_KINDS } from "../types/index.js";
|
|
6
6
|
/**
|
|
7
7
|
* Hand-maintained provider identity, credentials, defaults, and runtime
|
|
8
|
-
* behavior classification for every provider EXCEPT the
|
|
8
|
+
* behavior classification for every provider EXCEPT the 15 JSON-catalog
|
|
9
9
|
* providers derived by buildCatalogDescriptors() below (see
|
|
10
|
-
* PROVIDER_DESCRIPTORS's own doc for why Mistral,
|
|
11
|
-
* stays here too). Pure data — no provider-class imports, no dynamic
|
|
10
|
+
* PROVIDER_DESCRIPTORS's own doc for why Mistral, the remaining catalog
|
|
11
|
+
* provider, stays here too). Pure data — no provider-class imports, no dynamic
|
|
12
12
|
* import(), no side effects beyond building the two derived lookup maps
|
|
13
13
|
* below. Order follows the AIProviderName enum declaration order
|
|
14
14
|
* (enums.ts:8-40) so this file stays easy to diff against it.
|
|
@@ -496,13 +496,14 @@ const HAND_DESCRIPTORS = [
|
|
|
496
496
|
},
|
|
497
497
|
];
|
|
498
498
|
/**
|
|
499
|
-
* Builds ProviderDescriptor entries for
|
|
500
|
-
*
|
|
501
|
-
*
|
|
499
|
+
* Builds ProviderDescriptor entries for 15 of the 16 JSON-catalog providers —
|
|
500
|
+
* api-route, baseten, cerebras, cloudflare, fireworks, gmicloud, groq,
|
|
501
|
+
* inception-labs, io-intelligence, mancer, perplexity, sambanova,
|
|
502
|
+
* together-ai, upstage, xai. Every field is derived from the catalog JSON
|
|
502
503
|
* (src/lib/providers/catalog/<id>.json), never hand-typed.
|
|
503
504
|
*
|
|
504
|
-
* Mistral is the
|
|
505
|
-
* stays in HAND_DESCRIPTORS above: its JSON setup.url
|
|
505
|
+
* Mistral is the remaining catalog provider but is deliberately excluded
|
|
506
|
+
* here and stays in HAND_DESCRIPTORS above: its JSON setup.url
|
|
506
507
|
* ("https://console.mistral.ai/") diverges from the long-shipped descriptor
|
|
507
508
|
* setupUrl ("https://console.mistral.ai/api-keys" — a real value conflict,
|
|
508
509
|
* not missing enrichment), its envVars has no `model` key the JSON would
|
|
@@ -539,11 +540,12 @@ function buildCatalogDescriptor(entry) {
|
|
|
539
540
|
credentialsKey: catalogCredentialsKey(entry),
|
|
540
541
|
envVars,
|
|
541
542
|
defaultModel: entry.models.default,
|
|
542
|
-
//
|
|
543
|
-
//
|
|
544
|
-
// literal — it's the union's own
|
|
543
|
+
// 15 of the 16 catalog providers have capabilities.tools: true; Mancer
|
|
544
|
+
// ships tools: false, so this ternary's false branch is live, not
|
|
545
|
+
// hypothetical. "none" (not an invented literal — it's the union's own
|
|
546
|
+
// no-tool-support member, the same one
|
|
545
547
|
// REPLICATE/VOYAGE/JINA/STABILITY/IDEOGRAM/RECRAFT use above) is the
|
|
546
|
-
// correct false-branch
|
|
548
|
+
// correct false-branch here.
|
|
547
549
|
toolSupport: entry.capabilities.tools ? "native" : "none",
|
|
548
550
|
localRuntime: false,
|
|
549
551
|
healthCheck: "env-only",
|
|
@@ -561,7 +563,7 @@ function buildCatalogDescriptors() {
|
|
|
561
563
|
/**
|
|
562
564
|
* Single source of truth for provider identity, credentials, defaults, and
|
|
563
565
|
* runtime behavior classification — the hand-maintained providers plus the
|
|
564
|
-
*
|
|
566
|
+
* 15 JSON-catalog providers derived above.
|
|
565
567
|
*/
|
|
566
568
|
export const PROVIDER_DESCRIPTORS = [
|
|
567
569
|
...HAND_DESCRIPTORS,
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
*/
|
|
28
28
|
import { withTimeout } from "../utils/errorHandling.js";
|
|
29
29
|
import { DEFAULT_IMAGE_GEN_CONFIG } from "../types/index.js";
|
|
30
|
+
import { detectImageMimeType, sniffImageMimeType, } from "../utils/imageDetection.js";
|
|
30
31
|
/**
|
|
31
32
|
* NeuroLink instance type (avoiding circular dependencies)
|
|
32
33
|
*/
|
|
@@ -233,9 +234,14 @@ export class ImageGenService {
|
|
|
233
234
|
const img = imageOutput;
|
|
234
235
|
if (typeof img.base64 === "string") {
|
|
235
236
|
const base64 = img.base64;
|
|
236
|
-
const
|
|
237
|
+
const imageBuffer = Buffer.from(base64, "base64");
|
|
238
|
+
// Providers that do not label the format get it sniffed from the
|
|
239
|
+
// bytes rather than assumed to be PNG (Recraft, for one, returns WebP).
|
|
240
|
+
const mimeType = typeof img.mimeType === "string"
|
|
241
|
+
? img.mimeType
|
|
242
|
+
: detectImageMimeType(imageBuffer);
|
|
237
243
|
return {
|
|
238
|
-
imageBuffer
|
|
244
|
+
imageBuffer,
|
|
239
245
|
base64,
|
|
240
246
|
mimeType,
|
|
241
247
|
};
|
|
@@ -247,7 +253,7 @@ export class ImageGenService {
|
|
|
247
253
|
return {
|
|
248
254
|
imageBuffer: imageOutput,
|
|
249
255
|
base64,
|
|
250
|
-
mimeType:
|
|
256
|
+
mimeType: detectImageMimeType(imageOutput),
|
|
251
257
|
};
|
|
252
258
|
}
|
|
253
259
|
// Check for data URI format in content
|
|
@@ -269,20 +275,16 @@ export class ImageGenService {
|
|
|
269
275
|
// Try to detect if it's base64 encoded image data
|
|
270
276
|
try {
|
|
271
277
|
const buffer = Buffer.from(content, "base64");
|
|
272
|
-
//
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
};
|
|
279
|
-
}
|
|
280
|
-
// Check for JPEG magic bytes
|
|
281
|
-
if (buffer[0] === 0xff && buffer[1] === 0xd8) {
|
|
278
|
+
// Only treat the content as an image when it carries a known
|
|
279
|
+
// signature. `detectImageMimeType` answers `image/png` for bytes it
|
|
280
|
+
// does not recognise, so asking it here would turn arbitrary text into
|
|
281
|
+
// an image result; `sniffImageMimeType` says "no" instead.
|
|
282
|
+
const sniffed = sniffImageMimeType(buffer);
|
|
283
|
+
if (sniffed !== null) {
|
|
282
284
|
return {
|
|
283
285
|
imageBuffer: buffer,
|
|
284
286
|
base64: content,
|
|
285
|
-
mimeType:
|
|
287
|
+
mimeType: sniffed,
|
|
286
288
|
};
|
|
287
289
|
}
|
|
288
290
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -73,6 +73,7 @@ export { BeatovenMusic, BeatovenMusicHandler, ElevenLabsMusic, ElevenLabsMusicHa
|
|
|
73
73
|
export { DIDAvatar, DIDAvatarHandler, HeyGenAvatar, HeyGenAvatarHandler, registerDefaultAvatarHandlers, ReplicateAvatar, ReplicateAvatarHandler, } from "./avatar/index.js";
|
|
74
74
|
export { isVertexVideoConfigured, KlingVideoHandler, registerDefaultVideoHandlers, ReplicateVideoHandler, RunwayVideoHandler, VertexVideoHandler, } from "./adapters/video/index.js";
|
|
75
75
|
export { ImageGenService } from "./image-gen/ImageGenService.js";
|
|
76
|
+
export { detectImageMimeType, sniffImageMimeType, } from "./utils/imageDetection.js";
|
|
76
77
|
export { HITLManager } from "./hitl/hitlManager.js";
|
|
77
78
|
export { ProviderRegistry } from "./factories/providerRegistry.js";
|
|
78
79
|
import { NeuroLink } from "./neurolink.js";
|
package/dist/index.js
CHANGED
|
@@ -147,6 +147,7 @@ export { DIDAvatar, DIDAvatarHandler, HeyGenAvatar, HeyGenAvatarHandler, registe
|
|
|
147
147
|
export { isVertexVideoConfigured, KlingVideoHandler, registerDefaultVideoHandlers, ReplicateVideoHandler, RunwayVideoHandler, VertexVideoHandler, } from "./adapters/video/index.js";
|
|
148
148
|
// Image generation + HITL — surfaced from their dedicated barrels
|
|
149
149
|
export { ImageGenService } from "./image-gen/ImageGenService.js";
|
|
150
|
+
export { detectImageMimeType, sniffImageMimeType, } from "./utils/imageDetection.js";
|
|
150
151
|
export { HITLManager } from "./hitl/hitlManager.js";
|
|
151
152
|
// Provider registry (for tests, advanced consumers, and tools that need to
|
|
152
153
|
// invoke registerAllProviders() outside of constructing a NeuroLink instance)
|
|
@@ -92,6 +92,15 @@ export declare class ExternalServerManager extends EventEmitter {
|
|
|
92
92
|
* Start an external MCP server
|
|
93
93
|
*/
|
|
94
94
|
private startServer;
|
|
95
|
+
/**
|
|
96
|
+
* Close a live client/transport pair, clearing the lifecycle hooks first
|
|
97
|
+
* so the `onclose` they would otherwise trigger cannot schedule a restart
|
|
98
|
+
* of a connection being torn down deliberately. Shared by `stopServer` and
|
|
99
|
+
* the minTools readiness gate in `startServer`, which rejects a server
|
|
100
|
+
* before it is ever registered as connected. Leaves status untouched —
|
|
101
|
+
* callers set whatever status fits their case.
|
|
102
|
+
*/
|
|
103
|
+
private closeInstanceConnection;
|
|
95
104
|
/**
|
|
96
105
|
* Stop an external MCP server
|
|
97
106
|
*/
|
|
@@ -61,6 +61,29 @@ function isConnectionLostError(error) {
|
|
|
61
61
|
const message = error instanceof Error ? error.message : String(error);
|
|
62
62
|
return CONNECTION_LOST_PATTERN.test(message);
|
|
63
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* Thrown by `startServer` when a server's discovered tool count falls below
|
|
66
|
+
* its configured `minTools` floor. Deliberately thrown *inside* the same
|
|
67
|
+
* try/catch that already handles every other start failure, so the existing
|
|
68
|
+
* machinery (status -> "failed", `lastError`, span exception recording,
|
|
69
|
+
* rethrow to `addServer`'s own catch) applies unchanged; `addServer`
|
|
70
|
+
* recognizes this specific error via `instanceof` to attach the
|
|
71
|
+
* `readiness: "insufficient_tools"` + `toolsDiscovered` metadata that a
|
|
72
|
+
* generic start failure does not carry.
|
|
73
|
+
*/
|
|
74
|
+
class InsufficientToolsError extends Error {
|
|
75
|
+
serverId;
|
|
76
|
+
toolsDiscovered;
|
|
77
|
+
minTools;
|
|
78
|
+
readiness = "insufficient_tools";
|
|
79
|
+
constructor(serverId, toolsDiscovered, minTools) {
|
|
80
|
+
super(`Server '${serverId}' discovered ${toolsDiscovered} tool(s), below the configured minTools=${minTools}`);
|
|
81
|
+
this.serverId = serverId;
|
|
82
|
+
this.toolsDiscovered = toolsDiscovered;
|
|
83
|
+
this.minTools = minTools;
|
|
84
|
+
this.name = "InsufficientToolsError";
|
|
85
|
+
}
|
|
86
|
+
}
|
|
64
87
|
/**
|
|
65
88
|
* Consecutive idle health checks a server may fail to answer before it is
|
|
66
89
|
* treated as hung and restarted. Two, not one: a single missed ping right
|
|
@@ -639,6 +662,7 @@ export class ExternalServerManager extends EventEmitter {
|
|
|
639
662
|
retries: config.retries,
|
|
640
663
|
healthCheckInterval: config.healthCheckInterval,
|
|
641
664
|
autoRestart: config.autoRestart,
|
|
665
|
+
minTools: config.minTools,
|
|
642
666
|
cwd: config.cwd,
|
|
643
667
|
url: config.url,
|
|
644
668
|
metadata: {
|
|
@@ -680,6 +704,7 @@ export class ExternalServerManager extends EventEmitter {
|
|
|
680
704
|
retries: serverInfo.retries,
|
|
681
705
|
healthCheckInterval: serverInfo.healthCheckInterval,
|
|
682
706
|
autoRestart: serverInfo.autoRestart,
|
|
707
|
+
minTools: serverInfo.minTools,
|
|
683
708
|
cwd: serverInfo.cwd,
|
|
684
709
|
url: serverInfo.url,
|
|
685
710
|
blockedTools: serverInfo.blockedTools,
|
|
@@ -768,6 +793,7 @@ export class ExternalServerManager extends EventEmitter {
|
|
|
768
793
|
metadata: {
|
|
769
794
|
timestamp: Date.now(),
|
|
770
795
|
operation: "addServer",
|
|
796
|
+
readiness: "ready",
|
|
771
797
|
toolsDiscovered: finalInstance.tools.length,
|
|
772
798
|
},
|
|
773
799
|
};
|
|
@@ -778,6 +804,23 @@ export class ExternalServerManager extends EventEmitter {
|
|
|
778
804
|
mcpLogger.debug(`[ExternalServerManager] Failed to add server ${serverId}:`, error);
|
|
779
805
|
// Clean up if instance was created
|
|
780
806
|
this.servers.delete(serverId);
|
|
807
|
+
// The minTools readiness gate carries structured metadata a generic
|
|
808
|
+
// start failure does not: how many tools were actually discovered, so
|
|
809
|
+
// a caller can distinguish "too few tools" from "could not connect".
|
|
810
|
+
if (error instanceof InsufficientToolsError) {
|
|
811
|
+
return {
|
|
812
|
+
success: false,
|
|
813
|
+
error: error.message,
|
|
814
|
+
serverId,
|
|
815
|
+
duration: Date.now() - startTime,
|
|
816
|
+
metadata: {
|
|
817
|
+
timestamp: Date.now(),
|
|
818
|
+
operation: "addServer",
|
|
819
|
+
readiness: error.readiness,
|
|
820
|
+
toolsDiscovered: error.toolsDiscovered,
|
|
821
|
+
},
|
|
822
|
+
};
|
|
823
|
+
}
|
|
781
824
|
return {
|
|
782
825
|
success: false,
|
|
783
826
|
error: error instanceof Error ? error.message : String(error),
|
|
@@ -885,6 +928,22 @@ export class ExternalServerManager extends EventEmitter {
|
|
|
885
928
|
this.updateServerStatus(serverId, "connected");
|
|
886
929
|
// Discover tools from the server
|
|
887
930
|
await this.discoverServerTools(serverId);
|
|
931
|
+
// Readiness gate: a server whose discovered tool count falls below
|
|
932
|
+
// its configured minTools (default 0 — no minimum, so resource/prompt
|
|
933
|
+
// -only servers stay valid) is torn back down instead of being marked
|
|
934
|
+
// connected/healthy. Thrown here — before tools are registered with
|
|
935
|
+
// the main registry and before health monitoring starts — so a
|
|
936
|
+
// rejected server is never marked ready even transiently, and the
|
|
937
|
+
// throw is caught by this method's own catch below, which sets status
|
|
938
|
+
// "failed" and rethrows to addServer's catch the same way any other
|
|
939
|
+
// start failure does.
|
|
940
|
+
const minTools = config.minTools ?? 0;
|
|
941
|
+
if (instance.toolsMap.size < minTools) {
|
|
942
|
+
const toolsDiscovered = instance.toolsMap.size;
|
|
943
|
+
mcpLogger.warn(`[ExternalServerManager] Server '${serverId}' discovered ${toolsDiscovered} tool(s), below the configured minTools=${minTools}; tearing the connection back down`);
|
|
944
|
+
await this.closeInstanceConnection(serverId, instance);
|
|
945
|
+
throw new InsufficientToolsError(serverId, toolsDiscovered, minTools);
|
|
946
|
+
}
|
|
888
947
|
// Register tools with main registry if integration is enabled
|
|
889
948
|
if (this.enableMainRegistryIntegration) {
|
|
890
949
|
await this.registerServerToolsWithMainRegistry(serverId);
|
|
@@ -920,6 +979,31 @@ export class ExternalServerManager extends EventEmitter {
|
|
|
920
979
|
span.end();
|
|
921
980
|
}
|
|
922
981
|
}
|
|
982
|
+
/**
|
|
983
|
+
* Close a live client/transport pair, clearing the lifecycle hooks first
|
|
984
|
+
* so the `onclose` they would otherwise trigger cannot schedule a restart
|
|
985
|
+
* of a connection being torn down deliberately. Shared by `stopServer` and
|
|
986
|
+
* the minTools readiness gate in `startServer`, which rejects a server
|
|
987
|
+
* before it is ever registered as connected. Leaves status untouched —
|
|
988
|
+
* callers set whatever status fits their case.
|
|
989
|
+
*/
|
|
990
|
+
async closeInstanceConnection(serverId, instance) {
|
|
991
|
+
if (!instance.client || !instance.transportInstance) {
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
instance.client.onclose = undefined;
|
|
995
|
+
instance.client.onerror = undefined;
|
|
996
|
+
try {
|
|
997
|
+
await MCPClientFactory.closeClient(instance.client, instance.transportInstance);
|
|
998
|
+
}
|
|
999
|
+
catch (error) {
|
|
1000
|
+
mcpLogger.debug(`[ExternalServerManager] Error closing client for ${serverId}:`, error);
|
|
1001
|
+
}
|
|
1002
|
+
instance.client = null;
|
|
1003
|
+
instance.transportInstance = null;
|
|
1004
|
+
instance.process = null;
|
|
1005
|
+
instance.pid = undefined;
|
|
1006
|
+
}
|
|
923
1007
|
/**
|
|
924
1008
|
* Stop an external MCP server
|
|
925
1009
|
*/
|
|
@@ -951,22 +1035,10 @@ export class ExternalServerManager extends EventEmitter {
|
|
|
951
1035
|
// Clear server tools from discovery service
|
|
952
1036
|
this.toolDiscovery.clearServerTools(serverId);
|
|
953
1037
|
// Close MCP client using factory cleanup. This close is ours, not a
|
|
954
|
-
// crash:
|
|
955
|
-
// cannot schedule a restart of a server we
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
instance.client.onerror = undefined;
|
|
959
|
-
try {
|
|
960
|
-
await MCPClientFactory.closeClient(instance.client, instance.transportInstance);
|
|
961
|
-
}
|
|
962
|
-
catch (error) {
|
|
963
|
-
mcpLogger.debug(`[ExternalServerManager] Error closing client for ${serverId}:`, error);
|
|
964
|
-
}
|
|
965
|
-
instance.client = null;
|
|
966
|
-
instance.transportInstance = null;
|
|
967
|
-
instance.process = null;
|
|
968
|
-
instance.pid = undefined;
|
|
969
|
-
}
|
|
1038
|
+
// crash: closeInstanceConnection drops the lifecycle hooks first so
|
|
1039
|
+
// the onclose it triggers cannot schedule a restart of a server we
|
|
1040
|
+
// are deliberately stopping.
|
|
1041
|
+
await this.closeInstanceConnection(serverId, instance);
|
|
970
1042
|
this.updateServerStatus(serverId, "stopped");
|
|
971
1043
|
span.setStatus({ code: SpanStatusCode.OK });
|
|
972
1044
|
mcpLogger.info(`[ExternalServerManager] Server stopped: ${serverId}`);
|
package/dist/neurolink.js
CHANGED
|
@@ -7696,6 +7696,24 @@ Current user's request: ${currentInput}`;
|
|
|
7696
7696
|
factoryResult,
|
|
7697
7697
|
sessionId: enhancedOptions.context?.sessionId,
|
|
7698
7698
|
});
|
|
7699
|
+
// Curator P2-4 dedup (concurrency-safe): native provider stream paths
|
|
7700
|
+
// emit `generation:end` themselves. We attach a per-stream mutable
|
|
7701
|
+
// flag to `enhancedOptions._streamDedupContext`; the provider flips it
|
|
7702
|
+
// before its own emit and the finally block below reads the same
|
|
7703
|
+
// closed-over reference, so the orchestration does not emit a second
|
|
7704
|
+
// event for the same stream. Concurrent streams have different option
|
|
7705
|
+
// objects and therefore different contexts, so they cannot interfere.
|
|
7706
|
+
//
|
|
7707
|
+
// #1741: this MUST be attached before `createMCPStream`. That call
|
|
7708
|
+
// reaches `provider.stream({ ...options })`, which copies the options
|
|
7709
|
+
// — so a context attached afterwards never reaches the provider, and
|
|
7710
|
+
// every native stream path silently emitted twice. The flag looked
|
|
7711
|
+
// present on `enhancedOptions` and was simply absent from the object
|
|
7712
|
+
// the provider actually held.
|
|
7713
|
+
const dedupContext = {
|
|
7714
|
+
providerEmitted: false,
|
|
7715
|
+
};
|
|
7716
|
+
enhancedOptions._streamDedupContext = dedupContext;
|
|
7699
7717
|
// Kept whole (not just destructured) so its toolsUsed/toolExecutions
|
|
7700
7718
|
// descriptors can be re-applied onto the final StreamResult near the
|
|
7701
7719
|
// bottom of this method, without ever reading — and thereby freezing —
|
|
@@ -7733,17 +7751,6 @@ Current user's request: ${currentInput}`;
|
|
|
7733
7751
|
const streamStartTime = Date.now();
|
|
7734
7752
|
const sessionId = enhancedOptions.context
|
|
7735
7753
|
?.sessionId;
|
|
7736
|
-
// Curator P2-4 dedup (concurrency-safe): native provider stream paths
|
|
7737
|
-
// (Gemini 3 on Vertex / Google AI Studio) emit `generation:end`
|
|
7738
|
-
// themselves. We attach a per-stream mutable flag directly to
|
|
7739
|
-
// `enhancedOptions._streamDedupContext` — native providers receive
|
|
7740
|
-
// these options and flip the flag before their emit; this finally
|
|
7741
|
-
// block reads the same closed-over reference. Concurrent streams
|
|
7742
|
-
// have different option objects so the contexts don't interfere.
|
|
7743
|
-
const dedupContext = {
|
|
7744
|
-
providerEmitted: false,
|
|
7745
|
-
};
|
|
7746
|
-
enhancedOptions._streamDedupContext = dedupContext;
|
|
7747
7754
|
const processedStream = (async function* () {
|
|
7748
7755
|
let streamError;
|
|
7749
7756
|
// Curator P2-4: hoist `resolvedUsage` so the finally block can emit a
|
|
@@ -261,6 +261,15 @@ export class AmazonBedrockProvider extends BaseProvider {
|
|
|
261
261
|
success: false,
|
|
262
262
|
error: error instanceof Error ? error.message : String(error),
|
|
263
263
|
});
|
|
264
|
+
// #1741: the SDK emits its own `generation:end` unless the native
|
|
265
|
+
// path marks that it already did. The failure branch has no result
|
|
266
|
+
// to carry the mark, so it goes on the error — the same shape the
|
|
267
|
+
// AI Studio path uses. Without it one failed Bedrock generate is
|
|
268
|
+
// counted twice in analytics and as two Pipeline B spans.
|
|
269
|
+
if (error && typeof error === "object") {
|
|
270
|
+
error._generationEndEmitted =
|
|
271
|
+
true;
|
|
272
|
+
}
|
|
264
273
|
}
|
|
265
274
|
throw error;
|
|
266
275
|
}
|
|
@@ -288,7 +297,7 @@ export class AmazonBedrockProvider extends BaseProvider {
|
|
|
288
297
|
// `baseProvider` fell back to `result.toolsUsed || []` and every turn
|
|
289
298
|
// reported zero tools while running them correctly — leaving analytics,
|
|
290
299
|
// cost attribution and audit blind on this path.
|
|
291
|
-
|
|
300
|
+
const generateResult = {
|
|
292
301
|
content: text, // CLI expects 'content' not 'text'
|
|
293
302
|
usage,
|
|
294
303
|
model: this.modelName || this.getDefaultModel(),
|
|
@@ -320,6 +329,16 @@ export class AmazonBedrockProvider extends BaseProvider {
|
|
|
320
329
|
toolExecutions: resolveToolExecutionRecords(options, transformToolExecutions(toolExecutions)),
|
|
321
330
|
enhancedWithTools: toolExecutions.some((execution) => execution.error === undefined),
|
|
322
331
|
};
|
|
332
|
+
// #1741: the SDK's `finalizeGenerateRequestResult` emits its own
|
|
333
|
+
// `generation:end` unless the result carries this mark, so every
|
|
334
|
+
// SUCCESSFUL Bedrock generate was counted twice — an extra Langfuse
|
|
335
|
+
// `model.generation` observation and an extra analytics record per call,
|
|
336
|
+
// which inflates Bedrock's cost against every other provider. Vertex and
|
|
337
|
+
// AI Studio have always set it; Bedrock emitted without marking. Set
|
|
338
|
+
// after construction, like those two, because the field is deliberately
|
|
339
|
+
// not part of the public `EnhancedGenerateResult` surface.
|
|
340
|
+
generateResult._generationEndEmitted = true;
|
|
341
|
+
return generateResult;
|
|
323
342
|
}
|
|
324
343
|
async conversationLoop(options) {
|
|
325
344
|
// The step cap is now the same `maxSteps || DEFAULT_MAX_STEPS` the
|
|
@@ -1072,6 +1091,16 @@ export class AmazonBedrockProvider extends BaseProvider {
|
|
|
1072
1091
|
// never injected and generation:end is emitted by hand for
|
|
1073
1092
|
// Pipeline B (Langfuse).
|
|
1074
1093
|
if (streamEmitter) {
|
|
1094
|
+
// #1741: tell the orchestration's finally block this stream's
|
|
1095
|
+
// `generation:end` has already been emitted here, exactly as the
|
|
1096
|
+
// other native stream paths do. The context object is attached to
|
|
1097
|
+
// the caller's options by `NeuroLink.stream()`; BaseProvider
|
|
1098
|
+
// spreads the options but carries the same context reference, so
|
|
1099
|
+
// mutating it is what the orchestration reads back.
|
|
1100
|
+
const dedupContext = options._streamDedupContext;
|
|
1101
|
+
if (dedupContext) {
|
|
1102
|
+
dedupContext.providerEmitted = true;
|
|
1103
|
+
}
|
|
1075
1104
|
streamEmitter.emit("generation:end", {
|
|
1076
1105
|
provider: self.providerName,
|
|
1077
1106
|
responseTime: Date.now() - startTime,
|