@juspay/neurolink 13.7.0 → 13.8.1
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 +3 -6
- package/dist/browser/neurolink.min.js +480 -460
- package/dist/cli/commands/serve.js +11 -1
- package/dist/cli/commands/transcribe.js +39 -3
- package/dist/constants/langfuse.d.ts +2 -0
- package/dist/constants/langfuse.js +2 -0
- package/dist/factories/sttDescriptors.js +7 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +4 -2
- package/dist/mcp/externalServerManager.js +4 -1
- package/dist/mcp/mcpClientFactory.d.ts +6 -0
- package/dist/mcp/mcpClientFactory.js +44 -0
- package/dist/neurolink.js +42 -3
- package/dist/observability/exporters/langfuseExporter.d.ts +4 -0
- package/dist/observability/exporters/langfuseExporter.js +29 -8
- package/dist/observability/otlpEncoding.js +2 -2
- package/dist/server/abstract/baseServerAdapter.js +2 -0
- package/dist/server/adapters/expressAdapter.js +1 -1
- package/dist/server/adapters/fastifyAdapter.js +4 -1
- package/dist/server/adapters/honoAdapter.js +1 -1
- package/dist/server/adapters/koaAdapter.js +1 -1
- package/dist/server/routes/transcribeRoutes.d.ts +0 -8
- package/dist/server/routes/transcribeRoutes.js +130 -3
- package/dist/server/utils/validation.d.ts +409 -15
- package/dist/server/utils/validation.js +106 -4
- package/dist/server/websocket/frameQueue.d.ts +16 -0
- package/dist/server/websocket/frameQueue.js +55 -0
- package/dist/server/websocket/realtimeTranscription.d.ts +48 -0
- package/dist/server/websocket/realtimeTranscription.js +603 -0
- package/dist/server/websocket/transcribeWebSocket.d.ts +17 -1
- package/dist/server/websocket/transcribeWebSocket.js +263 -64
- package/dist/services/server/ai/observability/langfuseSetup.js +6 -1
- package/dist/types/cli.d.ts +6 -0
- package/dist/types/exporter.d.ts +3 -1
- package/dist/types/server.d.ts +304 -2
- package/dist/types/stt.d.ts +38 -3
- package/dist/types/transcribe.d.ts +401 -5
- package/dist/voice/audioLevel.d.ts +14 -0
- package/dist/voice/audioLevel.js +60 -0
- package/dist/voice/codeMix.d.ts +40 -0
- package/dist/voice/codeMix.js +282 -0
- package/dist/voice/correction/align.d.ts +12 -0
- package/dist/voice/correction/align.js +48 -0
- package/dist/voice/correction/guards.d.ts +39 -0
- package/dist/voice/correction/guards.js +122 -0
- package/dist/voice/correction/index.d.ts +25 -3
- package/dist/voice/correction/index.js +132 -4
- package/dist/voice/correction/rewrite.d.ts +64 -3
- package/dist/voice/correction/rewrite.js +295 -25
- package/dist/voice/eval/sttEvalLangfuse.d.ts +14 -0
- package/dist/voice/eval/sttEvalLangfuse.js +182 -0
- package/dist/voice/eval/sttEvalStore.d.ts +27 -0
- package/dist/voice/eval/sttEvalStore.js +397 -0
- package/dist/voice/index.js +3 -0
- package/dist/voice/languages.d.ts +34 -0
- package/dist/voice/languages.js +290 -0
- package/dist/voice/modelAssets.d.ts +28 -0
- package/dist/voice/modelAssets.js +113 -0
- package/dist/voice/output/render.d.ts +32 -0
- package/dist/voice/output/render.js +368 -0
- package/dist/voice/providers/OpenAISTT.d.ts +11 -1
- package/dist/voice/providers/OpenAISTT.js +57 -6
- package/dist/voice/session/transcriptionSession.d.ts +55 -0
- package/dist/voice/session/transcriptionSession.js +225 -0
- package/dist/voice/streaming/chunkedStreamAdapter.d.ts +7 -0
- package/dist/voice/streaming/chunkedStreamAdapter.js +711 -28
- package/dist/voice/streaming/endpointer.d.ts +50 -17
- package/dist/voice/streaming/endpointer.js +115 -19
- package/dist/voice/transcribe.d.ts +18 -1
- package/dist/voice/transcribe.js +459 -18
- package/dist/voice/transcribeStream.d.ts +18 -1
- package/dist/voice/transcribeStream.js +413 -16
- package/dist/voice/vad/assets.d.ts +36 -0
- package/dist/voice/vad/assets.js +142 -0
- package/dist/voice/vad/frameScorer.d.ts +28 -0
- package/dist/voice/vad/frameScorer.js +90 -0
- package/dist/voice/vad/sileroVad.d.ts +43 -0
- package/dist/voice/vad/sileroVad.js +178 -0
- package/dist/voice/whistle/assets.d.ts +3 -4
- package/dist/voice/whistle/assets.js +11 -95
- package/docs-site/static/search-index.json +21 -6
- package/models/silero-vad/README.md +19 -0
- package/models/silero-vad/silero_vad.onnx +0 -0
- package/package.json +3 -1
|
@@ -224,6 +224,11 @@ export class ServeCommandFactory {
|
|
|
224
224
|
.option("cors", {
|
|
225
225
|
type: "boolean",
|
|
226
226
|
description: "Enable CORS middleware (default: true)",
|
|
227
|
+
})
|
|
228
|
+
.option("max-upload", {
|
|
229
|
+
type: "string",
|
|
230
|
+
alias: "maxUpload",
|
|
231
|
+
description: "Largest raw upload the server accepts (multipart / audio bodies), e.g. 64mb (default)",
|
|
227
232
|
})
|
|
228
233
|
.option("cors-origin", {
|
|
229
234
|
type: "array",
|
|
@@ -418,7 +423,12 @@ export class ServeCommandFactory {
|
|
|
418
423
|
: (fileConfig.rateLimit?.maxRequests ??
|
|
419
424
|
defaultConfig.rateLimit.maxRequests),
|
|
420
425
|
},
|
|
421
|
-
bodyParser:
|
|
426
|
+
bodyParser: argv.maxUpload?.trim()
|
|
427
|
+
? {
|
|
428
|
+
...(fileConfig.bodyParser ?? {}),
|
|
429
|
+
rawMaxSize: argv.maxUpload.trim(),
|
|
430
|
+
}
|
|
431
|
+
: fileConfig.bodyParser,
|
|
422
432
|
logging: fileConfig.logging,
|
|
423
433
|
// Transcription of a long recording through a remote engine can take
|
|
424
434
|
// longer than the 30 s default, so the request timeout is also
|
|
@@ -147,7 +147,8 @@ function buildCorrection(argv, hasDictionary) {
|
|
|
147
147
|
correction.transliterate = argv.transliterate;
|
|
148
148
|
}
|
|
149
149
|
if (argv.secondOpinion) {
|
|
150
|
-
correction.secondOpinion =
|
|
150
|
+
correction.secondOpinion =
|
|
151
|
+
argv.secondOpinion === "auto" ? "auto" : { provider: argv.secondOpinion };
|
|
151
152
|
}
|
|
152
153
|
if (argv.context) {
|
|
153
154
|
correction.context = argv.context;
|
|
@@ -177,8 +178,19 @@ function buildCommonOptions(argv, dictionary) {
|
|
|
177
178
|
...(correction ? { correction } : {}),
|
|
178
179
|
...(argv.fallback ? { fallback: { provider: argv.fallback } } : {}),
|
|
179
180
|
...(argv.timeout !== undefined ? { timeoutMs: argv.timeout } : {}),
|
|
181
|
+
...(outputLanguagesArg(argv.to)
|
|
182
|
+
? { output: { languages: outputLanguagesArg(argv.to) ?? [] } }
|
|
183
|
+
: {}),
|
|
180
184
|
};
|
|
181
185
|
}
|
|
186
|
+
/** `--to es,hi-Latn` (or the flag repeated) → the tags, or `undefined` when none. */
|
|
187
|
+
function outputLanguagesArg(value) {
|
|
188
|
+
const tags = (Array.isArray(value) ? value : value ? [value] : [])
|
|
189
|
+
.flatMap((v) => v.split(","))
|
|
190
|
+
.map((t) => t.trim())
|
|
191
|
+
.filter(Boolean);
|
|
192
|
+
return tags.length ? tags : undefined;
|
|
193
|
+
}
|
|
182
194
|
// ---------------------------------------------------------------------------
|
|
183
195
|
// Decoding for --stream
|
|
184
196
|
// ---------------------------------------------------------------------------
|
|
@@ -332,6 +344,7 @@ function trailLine(result) {
|
|
|
332
344
|
["second opinion", t.secondOpinionMs],
|
|
333
345
|
["decide", t.decideMs],
|
|
334
346
|
["rewrite", t.rewriteMs],
|
|
347
|
+
["outputs", t.outputMs],
|
|
335
348
|
["total", t.totalMs],
|
|
336
349
|
]
|
|
337
350
|
.filter((entry) => typeof entry[1] === "number")
|
|
@@ -470,6 +483,12 @@ function createCliTranscribeStreamSink(argv) {
|
|
|
470
483
|
process.stdout.write(chalk.gray(`[utterance ${event.utterance}: ${event.seconds.toFixed(1)}s no engine could read]\n`));
|
|
471
484
|
}
|
|
472
485
|
break;
|
|
486
|
+
case "output":
|
|
487
|
+
clearLive();
|
|
488
|
+
process.stdout.write(event.text
|
|
489
|
+
? `${chalk.magenta(` ${event.language}:`)} ${event.text}\n`
|
|
490
|
+
: chalk.gray(` ${event.language}: ${event.status}${event.reason ? ` (${event.reason})` : ""}\n`));
|
|
491
|
+
break;
|
|
473
492
|
case "error":
|
|
474
493
|
clearLive();
|
|
475
494
|
logger.error(chalk.red(`Error${event.utterance !== undefined ? ` (utterance ${event.utterance})` : ""}: ${event.message}`));
|
|
@@ -559,7 +578,11 @@ export const transcribeCommand = {
|
|
|
559
578
|
})
|
|
560
579
|
.option("second-opinion", {
|
|
561
580
|
type: "string",
|
|
562
|
-
describe:
|
|
581
|
+
describe: 'Second STT provider run on the same audio; the rewrite reconciles both ("auto": the --fallback engine)',
|
|
582
|
+
})
|
|
583
|
+
.option("to", {
|
|
584
|
+
type: "string",
|
|
585
|
+
describe: "Output languages, comma-separated BCP-47 tags: es, fr-CA, hi-Latn (romanized Hindi), source",
|
|
563
586
|
})
|
|
564
587
|
.option("fallback", {
|
|
565
588
|
type: "string",
|
|
@@ -572,6 +595,11 @@ export const transcribeCommand = {
|
|
|
572
595
|
.option("stream", {
|
|
573
596
|
type: "boolean",
|
|
574
597
|
describe: "Stream the file through transcribeStream (decoded to 16 kHz PCM16) and print live text",
|
|
598
|
+
})
|
|
599
|
+
.option("vad", {
|
|
600
|
+
type: "string",
|
|
601
|
+
choices: ["auto", "silero", "energy"],
|
|
602
|
+
describe: "With --stream: the speech detector that finds utterances (default auto: Silero when it loads, else the energy gate)",
|
|
575
603
|
})
|
|
576
604
|
.option("format", {
|
|
577
605
|
type: "string",
|
|
@@ -649,7 +677,10 @@ export const transcribeCommand = {
|
|
|
649
677
|
const events = neurolink.transcribeStream({
|
|
650
678
|
...options,
|
|
651
679
|
audio: pcmFrames(pcm),
|
|
652
|
-
streaming: {
|
|
680
|
+
streaming: {
|
|
681
|
+
sampleRate: TARGET_SAMPLE_RATE,
|
|
682
|
+
...(argv.vad ? { vad: { engine: argv.vad } } : {}),
|
|
683
|
+
},
|
|
653
684
|
});
|
|
654
685
|
for await (const event of events) {
|
|
655
686
|
sink.event(event);
|
|
@@ -701,6 +732,11 @@ export const transcribeCommand = {
|
|
|
701
732
|
return;
|
|
702
733
|
}
|
|
703
734
|
logger.always(result.text);
|
|
735
|
+
for (const rendering of Object.values(result.outputs ?? {})) {
|
|
736
|
+
logger.always(rendering.text
|
|
737
|
+
? `${chalk.magenta(`${rendering.language}:`)} ${rendering.text}`
|
|
738
|
+
: chalk.gray(`${rendering.language}: ${rendering.status}${rendering.reason ? ` (${rendering.reason})` : ""}`));
|
|
739
|
+
}
|
|
704
740
|
if (!argv.quiet) {
|
|
705
741
|
logger.always(chalk.gray(trailLine(result)));
|
|
706
742
|
}
|
|
@@ -21,6 +21,8 @@ export declare const MAX_CONCURRENT_DELIVERIES = 4;
|
|
|
21
21
|
export declare const MAX_QUEUED_SPANS = 10000;
|
|
22
22
|
/** @description Milliseconds between warn-level logs about waiting spans dropped for lack of a delivery slot. */
|
|
23
23
|
export declare const QUEUE_DROP_WARN_INTERVAL_MS = 60000;
|
|
24
|
+
/** @description Milliseconds between error-level logs about spans dropped because Langfuse rejected the credentials. */
|
|
25
|
+
export declare const CREDENTIALS_WARN_INTERVAL_MS = 60000;
|
|
24
26
|
/** @description Longest `Retry-After` a 429 or 503 from Langfuse is waited out before the retry. */
|
|
25
27
|
export declare const MAX_EXPORT_RETRY_AFTER_MS = 60000;
|
|
26
28
|
/** @description Maximum serialized bytes sent in one export request. */
|
|
@@ -21,6 +21,8 @@ export const MAX_CONCURRENT_DELIVERIES = 4;
|
|
|
21
21
|
export const MAX_QUEUED_SPANS = 10_000;
|
|
22
22
|
/** @description Milliseconds between warn-level logs about waiting spans dropped for lack of a delivery slot. */
|
|
23
23
|
export const QUEUE_DROP_WARN_INTERVAL_MS = 60_000;
|
|
24
|
+
/** @description Milliseconds between error-level logs about spans dropped because Langfuse rejected the credentials. */
|
|
25
|
+
export const CREDENTIALS_WARN_INTERVAL_MS = 60_000;
|
|
24
26
|
/** @description Longest `Retry-After` a 429 or 503 from Langfuse is waited out before the retry. */
|
|
25
27
|
export const MAX_EXPORT_RETRY_AFTER_MS = 60_000;
|
|
26
28
|
/** @description Maximum serialized bytes sent in one export request. */
|
|
@@ -246,6 +246,13 @@ function validateSTTEndpoints(source, entries) {
|
|
|
246
246
|
...(typeof c.apiKey === "string" ? { apiKey: c.apiKey } : {}),
|
|
247
247
|
...(typeof c.model === "string" ? { model: c.model } : {}),
|
|
248
248
|
...(typeof c.timeoutMs === "number" ? { timeoutMs: c.timeoutMs } : {}),
|
|
249
|
+
...(typeof c.priorLanguageForm === "string"
|
|
250
|
+
? { priorLanguageForm: c.priorLanguageForm }
|
|
251
|
+
: {}),
|
|
252
|
+
...(typeof c.lidModel === "string" ? { lidModel: c.lidModel } : {}),
|
|
253
|
+
...(typeof c.vocabularyPrompt === "boolean"
|
|
254
|
+
? { vocabularyPrompt: c.vocabularyPrompt }
|
|
255
|
+
: {}),
|
|
249
256
|
};
|
|
250
257
|
}
|
|
251
258
|
return out;
|
package/dist/index.d.ts
CHANGED
|
@@ -73,6 +73,7 @@ export { STTProcessor } from "./utils/sttProcessor.js";
|
|
|
73
73
|
export { describeSTTProviderKeys, isSTTProviderConfigured, resolveDefaultSTTProvider, STT_PROVIDER_DESCRIPTORS, } from "./factories/sttDescriptors.js";
|
|
74
74
|
export { buildSpeakerTurns, listSpeakers } from "./utils/speakerTurns.js";
|
|
75
75
|
export { correctTranscript } from "./voice/correction/index.js";
|
|
76
|
+
export { TranscriptionSession } from "./voice/session/transcriptionSession.js";
|
|
76
77
|
export { AzureTTS, AzureTTSHandler, CartesiaTTS, CartesiaTTSHandler, ElevenLabsTTS, ElevenLabsTTSHandler, FishAudioTTS, FishAudioTTSHandler, SixtyDBTTS, SixtyDBTTSHandler, GoogleTTSHandler, OpenAITTS, OpenAITTSHandler, AzureSTT, AzureSTTHandler, DeepgramSTT, DeepgramSTTHandler, ElevenLabsSTT, ElevenLabsSTTHandler, GoogleSTT, GoogleSTTHandler, OpenAISTT, OpenAISTTHandler, WhisperSTT, WhisperSTTHandler, WhistleSTT, WhistleSTTHandler, BaseRealtimeHandler, GeminiLive, GeminiLiveHandler, OpenAIRealtime, OpenAIRealtimeHandler, RealtimeProcessor, RealtimeError, STTError, VoiceError, registerDefaultRealtimeHandlers, registerDefaultSTTHandlers, registerDefaultTTSHandlers, } from "./voice/index.js";
|
|
77
78
|
export { BeatovenMusic, BeatovenMusicHandler, ElevenLabsMusic, ElevenLabsMusicHandler, LyriaMusic, LyriaMusicHandler, registerDefaultMusicHandlers, ReplicateMusic, ReplicateMusicHandler, } from "./music/index.js";
|
|
78
79
|
export { DIDAvatar, DIDAvatarHandler, HeyGenAvatar, HeyGenAvatarHandler, registerDefaultAvatarHandlers, ReplicateAvatar, ReplicateAvatarHandler, } from "./avatar/index.js";
|
|
@@ -84,8 +85,8 @@ export { HITLManager } from "./hitl/hitlManager.js";
|
|
|
84
85
|
export { ProviderRegistry } from "./factories/providerRegistry.js";
|
|
85
86
|
import { NeuroLink } from "./neurolink.js";
|
|
86
87
|
export { NeuroLink };
|
|
87
|
-
import { flushOpenTelemetry, getLangfuseHealthStatus, isOpenTelemetryInitialized, setLangfuseContext, shutdownOpenTelemetry } from "./services/server/ai/observability/instrumentation.js";
|
|
88
|
-
export { shutdownOpenTelemetry, flushOpenTelemetry, getLangfuseHealthStatus, setLangfuseContext, isOpenTelemetryInitialized, };
|
|
88
|
+
import { ensureTelemetry, flushOpenTelemetry, getLangfuseHealthStatus, isOpenTelemetryInitialized, setLangfuseContext, shutdownOpenTelemetry } from "./services/server/ai/observability/instrumentation.js";
|
|
89
|
+
export { ensureTelemetry, shutdownOpenTelemetry, flushOpenTelemetry, getLangfuseHealthStatus, setLangfuseContext, isOpenTelemetryInitialized, };
|
|
89
90
|
export { clearAnalyticsMetrics, createAnalyticsMiddleware, getAnalyticsMetrics, } from "./middleware/builtin/analytics.js";
|
|
90
91
|
export { createLifecycleMiddleware } from "./middleware/builtin/lifecycle.js";
|
|
91
92
|
export { MiddlewareFactory } from "./middleware/factory.js";
|
package/dist/index.js
CHANGED
|
@@ -126,6 +126,8 @@ export { STTProcessor } from "./utils/sttProcessor.js";
|
|
|
126
126
|
export { describeSTTProviderKeys, isSTTProviderConfigured, resolveDefaultSTTProvider, STT_PROVIDER_DESCRIPTORS, } from "./factories/sttDescriptors.js";
|
|
127
127
|
export { buildSpeakerTurns, listSpeakers } from "./utils/speakerTurns.js";
|
|
128
128
|
export { correctTranscript } from "./voice/correction/index.js";
|
|
129
|
+
// Transcription sessions: several speakers, listeners in their own languages.
|
|
130
|
+
export { TranscriptionSession } from "./voice/session/transcriptionSession.js";
|
|
129
131
|
// STT_ERROR_CODES is surfaced via the types barrel (export * from "./types/index.js")
|
|
130
132
|
// STTError is re-exported below from the voice/index.js barrel
|
|
131
133
|
// ============================================================================
|
|
@@ -172,8 +174,8 @@ export { ProviderRegistry } from "./factories/providerRegistry.js";
|
|
|
172
174
|
import { NeuroLink } from "./neurolink.js";
|
|
173
175
|
export { NeuroLink };
|
|
174
176
|
// Observability configuration types
|
|
175
|
-
import { flushOpenTelemetry, getLangfuseHealthStatus, isOpenTelemetryInitialized, setLangfuseContext, shutdownOpenTelemetry, } from "./services/server/ai/observability/instrumentation.js";
|
|
176
|
-
export { shutdownOpenTelemetry, flushOpenTelemetry, getLangfuseHealthStatus, setLangfuseContext, isOpenTelemetryInitialized, };
|
|
177
|
+
import { ensureTelemetry, flushOpenTelemetry, getLangfuseHealthStatus, isOpenTelemetryInitialized, setLangfuseContext, shutdownOpenTelemetry, } from "./services/server/ai/observability/instrumentation.js";
|
|
178
|
+
export { ensureTelemetry, shutdownOpenTelemetry, flushOpenTelemetry, getLangfuseHealthStatus, setLangfuseContext, isOpenTelemetryInitialized, };
|
|
177
179
|
// Analytics Middleware exports
|
|
178
180
|
export { clearAnalyticsMetrics, createAnalyticsMiddleware, getAnalyticsMetrics, } from "./middleware/builtin/analytics.js";
|
|
179
181
|
export { createLifecycleMiddleware } from "./middleware/builtin/lifecycle.js";
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { EventEmitter } from "events";
|
|
10
10
|
import { mcpLogger } from "../utils/logger.js";
|
|
11
|
-
import { MCPClientFactory } from "./mcpClientFactory.js";
|
|
11
|
+
import { MCPClientFactory, terminateStartingStdioServers, } from "./mcpClientFactory.js";
|
|
12
12
|
import { ToolDiscoveryService } from "./toolDiscoveryService.js";
|
|
13
13
|
import { toolRegistry } from "./toolRegistry.js";
|
|
14
14
|
import { HITLUserRejectedError, HITLTimeoutError } from "../hitl/hitlErrors.js";
|
|
@@ -372,6 +372,9 @@ const shutdownOnSignal = (signal) => {
|
|
|
372
372
|
// re-raise and nowhere else.
|
|
373
373
|
if (!hadOtherListeners) {
|
|
374
374
|
process.removeListener(signal, shutdownOnSignal);
|
|
375
|
+
// Dying of a signal runs no `exit` handler, and the cleanup above cannot
|
|
376
|
+
// reach a server whose handshake is still pending.
|
|
377
|
+
terminateStartingStdioServers();
|
|
375
378
|
process.kill(process.pid, signal);
|
|
376
379
|
}
|
|
377
380
|
});
|
|
@@ -9,6 +9,12 @@ import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
|
|
|
9
9
|
import type { ClientCapabilities } from "@modelcontextprotocol/sdk/types.js";
|
|
10
10
|
import type { ChildProcess } from "child_process";
|
|
11
11
|
import type { MCPTransportType, MCPServerInfo, MCPClientResult } from "../types/index.js";
|
|
12
|
+
/**
|
|
13
|
+
* Synchronous on purpose: it is the one thing that can run inside the
|
|
14
|
+
* `exit` event, and SIGTERM is delivered by the kernel even though this
|
|
15
|
+
* process is already on its way out.
|
|
16
|
+
*/
|
|
17
|
+
export declare const terminateStartingStdioServers: () => void;
|
|
12
18
|
/**
|
|
13
19
|
* MCPClientFactory
|
|
14
20
|
* Factory class for creating MCP clients with different transports
|
|
@@ -62,6 +62,48 @@ class StderrTail {
|
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
const stderrTails = new WeakMap();
|
|
65
|
+
/**
|
|
66
|
+
* Stdio servers that have been spawned but have not yet answered `initialize`.
|
|
67
|
+
*
|
|
68
|
+
* Nothing that cleans up at exit can reach one: the manager learns of a client
|
|
69
|
+
* and its transport only when `connect()` returns, and `process.exit()` runs
|
|
70
|
+
* no async handler anyway. It is also the state a server is least likely to
|
|
71
|
+
* notice its host going away, because `npx -y <pkg>` does not read its stdin
|
|
72
|
+
* until the package has downloaded. Once a server has answered it is an
|
|
73
|
+
* ordinary MCP server again and ends when its stdin closes, so it leaves this
|
|
74
|
+
* set and the shutdown paths that already exist keep ownership of it.
|
|
75
|
+
*/
|
|
76
|
+
const startingStdioTransports = new Set();
|
|
77
|
+
let startingStdioExitHookInstalled = false;
|
|
78
|
+
/**
|
|
79
|
+
* Synchronous on purpose: it is the one thing that can run inside the
|
|
80
|
+
* `exit` event, and SIGTERM is delivered by the kernel even though this
|
|
81
|
+
* process is already on its way out.
|
|
82
|
+
*/
|
|
83
|
+
export const terminateStartingStdioServers = () => {
|
|
84
|
+
for (const transport of startingStdioTransports) {
|
|
85
|
+
if ("pid" in transport && typeof transport.pid === "number") {
|
|
86
|
+
try {
|
|
87
|
+
process.kill(transport.pid, "SIGTERM");
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
// Already gone.
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
const trackStartingStdioTransport = (transport) => {
|
|
96
|
+
if (!startingStdioExitHookInstalled) {
|
|
97
|
+
startingStdioExitHookInstalled = true;
|
|
98
|
+
process.on("exit", terminateStartingStdioServers);
|
|
99
|
+
}
|
|
100
|
+
startingStdioTransports.add(transport);
|
|
101
|
+
// Client.connect() chains an onclose that is already set rather than
|
|
102
|
+
// replacing it, and a spawn that fails still closes the transport.
|
|
103
|
+
transport.onclose = () => {
|
|
104
|
+
startingStdioTransports.delete(transport);
|
|
105
|
+
};
|
|
106
|
+
};
|
|
65
107
|
/**
|
|
66
108
|
* MCPClientFactory
|
|
67
109
|
* Factory class for creating MCP clients with different transports
|
|
@@ -223,6 +265,7 @@ export class MCPClientFactory {
|
|
|
223
265
|
}
|
|
224
266
|
// Connect with timeout
|
|
225
267
|
await this.raceWithTimeout(client.connect(transport, { timeout }), timeout, `Client connection timeout for ${config.id}`);
|
|
268
|
+
startingStdioTransports.delete(transport);
|
|
226
269
|
// Perform handshake to get server capabilities
|
|
227
270
|
const serverCapabilities = await this.performHandshake(client, timeout);
|
|
228
271
|
mcpLogger.debug(`[MCPClientFactory] Handshake completed for ${config.id}`, {
|
|
@@ -318,6 +361,7 @@ export class MCPClientFactory {
|
|
|
318
361
|
cwd: config.cwd,
|
|
319
362
|
stderr: "pipe",
|
|
320
363
|
});
|
|
364
|
+
trackStartingStdioTransport(transport);
|
|
321
365
|
const tail = new StderrTail();
|
|
322
366
|
stderrTails.set(transport, tail);
|
|
323
367
|
transport.stderr?.on("data", (chunk) => {
|
package/dist/neurolink.js
CHANGED
|
@@ -3209,7 +3209,9 @@ Current user's request: ${currentInput}`;
|
|
|
3209
3209
|
langfuse: langfuseConfig !== undefined || hasProcessor
|
|
3210
3210
|
? {
|
|
3211
3211
|
enabled: hasProcessor,
|
|
3212
|
-
baseUrl: langfuseOptions?.baseUrl
|
|
3212
|
+
baseUrl: langfuseOptions?.baseUrl ||
|
|
3213
|
+
process.env.LANGFUSE_BASE_URL ||
|
|
3214
|
+
undefined,
|
|
3213
3215
|
environment: langfuseOptions?.environment ??
|
|
3214
3216
|
process.env.LANGFUSE_TRACING_ENVIRONMENT,
|
|
3215
3217
|
}
|
|
@@ -8991,8 +8993,6 @@ Current user's request: ${currentInput}`;
|
|
|
8991
8993
|
async prepareStreamOptions(options, streamId, startTime, hrTimeStart) {
|
|
8992
8994
|
// Initialize conversation memory if needed (for lazy loading)
|
|
8993
8995
|
await this.initializeConversationMemoryForGeneration(streamId, startTime, hrTimeStart);
|
|
8994
|
-
// Initialize MCP
|
|
8995
|
-
await this.initializeMCP();
|
|
8996
8996
|
// Memory retrieval
|
|
8997
8997
|
if (this.shouldReadMemory(options.memory, options.context?.userId) &&
|
|
8998
8998
|
options.context?.userId) {
|
|
@@ -9040,6 +9040,15 @@ Current user's request: ${currentInput}`;
|
|
|
9040
9040
|
}
|
|
9041
9041
|
// Auto-disable tools for Ollama models that don't support them
|
|
9042
9042
|
await this.autoDisableOllamaStreamTools(options);
|
|
9043
|
+
// generate() skips MCP when tools are disabled, so stream() must too:
|
|
9044
|
+
// initialization loads the working directory's .mcp-config.json and waits
|
|
9045
|
+
// for every stdio server it names before the request is sent, so an
|
|
9046
|
+
// `npx -y …` entry on a slow npm registry stalls a stream that never
|
|
9047
|
+
// needed a tool. Decided after the Ollama check above, which can turn
|
|
9048
|
+
// tools off. Providers register themselves in AIProviderFactory.
|
|
9049
|
+
if (!options.disableTools) {
|
|
9050
|
+
await this.initializeMCP();
|
|
9051
|
+
}
|
|
9043
9052
|
// RAG Integration: If rag config is provided, prepare the RAG search tool
|
|
9044
9053
|
if (options.rag?.files?.length) {
|
|
9045
9054
|
try {
|
|
@@ -14710,6 +14719,36 @@ Current user's request: ${currentInput}`;
|
|
|
14710
14719
|
clearTimeout(timer);
|
|
14711
14720
|
}
|
|
14712
14721
|
},
|
|
14722
|
+
render: async (o) => {
|
|
14723
|
+
const controller = new AbortController();
|
|
14724
|
+
const timer = setTimeout(() => controller.abort(), o.timeoutMs);
|
|
14725
|
+
// Output rendering is translation, often worth a stronger model than
|
|
14726
|
+
// the rewrite; it has its own setting and falls back to the rewrite's.
|
|
14727
|
+
const provider = o.provider ??
|
|
14728
|
+
(process.env.NEUROLINK_STT_OUTPUT_PROVIDER?.trim() ||
|
|
14729
|
+
process.env.NEUROLINK_STT_REWRITE_PROVIDER?.trim());
|
|
14730
|
+
const model = o.model ??
|
|
14731
|
+
(process.env.NEUROLINK_STT_OUTPUT_MODEL?.trim() ||
|
|
14732
|
+
process.env.NEUROLINK_STT_REWRITE_MODEL?.trim());
|
|
14733
|
+
try {
|
|
14734
|
+
const generated = await this.generate({
|
|
14735
|
+
input: { text: o.user },
|
|
14736
|
+
systemPrompt: o.system,
|
|
14737
|
+
schema: o.schema,
|
|
14738
|
+
...(provider ? { provider } : {}),
|
|
14739
|
+
...(model ? { model } : {}),
|
|
14740
|
+
maxTokens: o.maxTokens,
|
|
14741
|
+
temperature: 0,
|
|
14742
|
+
disableTools: true,
|
|
14743
|
+
memory: { enabled: false },
|
|
14744
|
+
abortSignal: controller.signal,
|
|
14745
|
+
});
|
|
14746
|
+
return generated.structuredData ?? generated.content ?? "";
|
|
14747
|
+
}
|
|
14748
|
+
finally {
|
|
14749
|
+
clearTimeout(timer);
|
|
14750
|
+
}
|
|
14751
|
+
},
|
|
14713
14752
|
};
|
|
14714
14753
|
}
|
|
14715
14754
|
/**
|
|
@@ -16,6 +16,9 @@ export declare class LangfuseExporter extends BaseExporter {
|
|
|
16
16
|
private lastDeliveryError;
|
|
17
17
|
private unwarnedQueueDrops;
|
|
18
18
|
private lastQueueDropWarning;
|
|
19
|
+
private unwarnedRefusals;
|
|
20
|
+
private lastRefusalWarning;
|
|
21
|
+
private credentialsRejected;
|
|
19
22
|
/**
|
|
20
23
|
* @description Creates an exporter for one Langfuse project.
|
|
21
24
|
* @param config {LangfuseExporterConfig} - Credentials, endpoint, environment, release, masking, batching and flush-budget options. `environment` and `release` fall back to `LANGFUSE_TRACING_ENVIRONMENT` and `LANGFUSE_RELEASE`, and only fill spans that do not carry them already.
|
|
@@ -47,6 +50,7 @@ export declare class LangfuseExporter extends BaseExporter {
|
|
|
47
50
|
getStatus(): ExporterStatus;
|
|
48
51
|
healthCheck(): Promise<ExporterHealthStatus>;
|
|
49
52
|
protected ping(): Promise<void>;
|
|
53
|
+
private reportRefusals;
|
|
50
54
|
private record;
|
|
51
55
|
private deliver;
|
|
52
56
|
private post;
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import { DEFAULT_BASE_URL, DEFAULT_FLUSH_AT, DEFAULT_FLUSH_BUDGET_MS, DEFAULT_FLUSH_INTERVAL_MS, DEFAULT_TIMEOUT_MS, HEALTH_PATH, MAX_CONCURRENT_DELIVERIES, MAX_EXPORT_RETRY_AFTER_MS, MAX_QUEUED_SPANS, MAX_SPANS_PER_REQUEST, QUEUE_DROP_WARN_INTERVAL_MS, TRACES_PATH, } from "../../constants/langfuse.js";
|
|
1
|
+
import { CREDENTIALS_WARN_INTERVAL_MS, DEFAULT_BASE_URL, DEFAULT_FLUSH_AT, DEFAULT_FLUSH_BUDGET_MS, DEFAULT_FLUSH_INTERVAL_MS, DEFAULT_TIMEOUT_MS, HEALTH_PATH, MAX_CONCURRENT_DELIVERIES, MAX_EXPORT_RETRY_AFTER_MS, MAX_QUEUED_SPANS, MAX_SPANS_PER_REQUEST, QUEUE_DROP_WARN_INTERVAL_MS, TRACES_PATH, } from "../../constants/langfuse.js";
|
|
2
2
|
import { logger } from "../../utils/logger.js";
|
|
3
3
|
import { parseRetryAfterMs } from "../../utils/retryAfter.js";
|
|
4
4
|
import { chunkOtlpSpans, toOtlpEncodedSpan, toOtlpRequest, } from "../otlpEncoding.js";
|
|
5
5
|
import { BaseExporter, exponentialRetryDelayMs, exporterStatusOf, reportDeliveriesWith, } from "./baseExporter.js";
|
|
6
|
-
let credentialsWarned = false;
|
|
7
6
|
let malformedRejectionLogged = false;
|
|
8
7
|
function isRetryable(status) {
|
|
9
8
|
return status === 429 || status >= 500;
|
|
@@ -73,6 +72,9 @@ export class LangfuseExporter extends BaseExporter {
|
|
|
73
72
|
lastDeliveryError;
|
|
74
73
|
unwarnedQueueDrops = 0;
|
|
75
74
|
lastQueueDropWarning = 0;
|
|
75
|
+
unwarnedRefusals = 0;
|
|
76
|
+
lastRefusalWarning = 0;
|
|
77
|
+
credentialsRejected = false;
|
|
76
78
|
/**
|
|
77
79
|
* @description Creates an exporter for one Langfuse project.
|
|
78
80
|
* @param config {LangfuseExporterConfig} - Credentials, endpoint, environment, release, masking, batching and flush-budget options. `environment` and `release` fall back to `LANGFUSE_TRACING_ENVIRONMENT` and `LANGFUSE_RELEASE`, and only fill spans that do not carry them already.
|
|
@@ -84,7 +86,7 @@ export class LangfuseExporter extends BaseExporter {
|
|
|
84
86
|
});
|
|
85
87
|
this.langfuse = langfuse;
|
|
86
88
|
this.authorization = `Basic ${Buffer.from(`${langfuse.publicKey}:${langfuse.secretKey}`).toString("base64")}`;
|
|
87
|
-
this.baseUrl = (langfuse.baseUrl
|
|
89
|
+
this.baseUrl = (langfuse.baseUrl || DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
88
90
|
this.flushBudgetMs =
|
|
89
91
|
langfuse.flushBudgetMs !== undefined && langfuse.flushBudgetMs > 0
|
|
90
92
|
? langfuse.flushBudgetMs
|
|
@@ -218,6 +220,9 @@ export class LangfuseExporter extends BaseExporter {
|
|
|
218
220
|
this.lastDeliveryError = "Langfuse exporter shut down before delivery";
|
|
219
221
|
logger.warn(`[langfuse] Dropped ${stranded} spans still waiting at shutdown`);
|
|
220
222
|
}
|
|
223
|
+
if (this.unwarnedRefusals > 0) {
|
|
224
|
+
this.reportRefusals();
|
|
225
|
+
}
|
|
221
226
|
}
|
|
222
227
|
/**
|
|
223
228
|
* @description Snapshot of this exporter for status reporting, without a network call: what is queued, what Langfuse accepted and what was dropped, and unhealthy while the latest delivery failed.
|
|
@@ -257,6 +262,10 @@ export class LangfuseExporter extends BaseExporter {
|
|
|
257
262
|
throw new Error(`Langfuse API unreachable: ${response.status}`);
|
|
258
263
|
}
|
|
259
264
|
}
|
|
265
|
+
reportRefusals() {
|
|
266
|
+
logger.error(`[langfuse] Langfuse credentials rejected; dropped ${this.unwarnedRefusals} spans`);
|
|
267
|
+
this.unwarnedRefusals = 0;
|
|
268
|
+
}
|
|
260
269
|
record(total, report, durationMs) {
|
|
261
270
|
if (total === 0) {
|
|
262
271
|
return this.createSuccessResult(0, durationMs);
|
|
@@ -269,7 +278,7 @@ export class LangfuseExporter extends BaseExporter {
|
|
|
269
278
|
report.delivered === 0 && failed > 0
|
|
270
279
|
? `Dropped ${failed} of ${total} spans: ${report.error ?? "Langfuse rejected every span"}`
|
|
271
280
|
: report.error;
|
|
272
|
-
if (failed >
|
|
281
|
+
if (failed > report.refused) {
|
|
273
282
|
logger.warn(`[langfuse] Dropped ${failed} of ${total} spans`, {
|
|
274
283
|
error: report.error,
|
|
275
284
|
});
|
|
@@ -296,6 +305,7 @@ export class LangfuseExporter extends BaseExporter {
|
|
|
296
305
|
const report = {
|
|
297
306
|
delivered: 0,
|
|
298
307
|
rejected: 0,
|
|
308
|
+
refused: 0,
|
|
299
309
|
droppedIds: [],
|
|
300
310
|
};
|
|
301
311
|
for (const chunk of chunkOtlpSpans(items)) {
|
|
@@ -308,6 +318,9 @@ export class LangfuseExporter extends BaseExporter {
|
|
|
308
318
|
else {
|
|
309
319
|
report.droppedIds.push(...chunk.map(({ span }) => span.spanId));
|
|
310
320
|
report.error = "Langfuse did not accept the batch";
|
|
321
|
+
if (outcome.refused) {
|
|
322
|
+
report.refused += chunk.length;
|
|
323
|
+
}
|
|
311
324
|
}
|
|
312
325
|
}
|
|
313
326
|
catch (error) {
|
|
@@ -334,11 +347,14 @@ export class LangfuseExporter extends BaseExporter {
|
|
|
334
347
|
]),
|
|
335
348
|
});
|
|
336
349
|
if (response.status === 401 || response.status === 403) {
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
350
|
+
this.credentialsRejected = true;
|
|
351
|
+
this.unwarnedRefusals += chunk.length;
|
|
352
|
+
const now = Date.now();
|
|
353
|
+
if (now - this.lastRefusalWarning >= CREDENTIALS_WARN_INTERVAL_MS) {
|
|
354
|
+
this.reportRefusals();
|
|
355
|
+
this.lastRefusalWarning = now;
|
|
340
356
|
}
|
|
341
|
-
return { accepted: false };
|
|
357
|
+
return { accepted: false, refused: true };
|
|
342
358
|
}
|
|
343
359
|
if (isRetryable(response.status)) {
|
|
344
360
|
const retryAfterMs = response.status === 429 || response.status === 503
|
|
@@ -350,6 +366,11 @@ export class LangfuseExporter extends BaseExporter {
|
|
|
350
366
|
logger.warn(`[langfuse] Export rejected with ${response.status}`);
|
|
351
367
|
return { accepted: false };
|
|
352
368
|
}
|
|
369
|
+
if (this.credentialsRejected) {
|
|
370
|
+
logger.error(`[langfuse] Langfuse credentials accepted again; dropped ${this.unwarnedRefusals} spans since the last warning`);
|
|
371
|
+
this.credentialsRejected = false;
|
|
372
|
+
this.unwarnedRefusals = 0;
|
|
373
|
+
}
|
|
353
374
|
// Langfuse took the request; a body that is not an object, or not JSON,
|
|
354
375
|
// reports no rejections.
|
|
355
376
|
const partial = partialSuccessOf(await response.json().catch(() => undefined));
|
|
@@ -13,8 +13,8 @@ function toOtlpValue(value) {
|
|
|
13
13
|
return { boolValue: value };
|
|
14
14
|
}
|
|
15
15
|
if (typeof value === "number") {
|
|
16
|
-
return Number.
|
|
17
|
-
? { intValue:
|
|
16
|
+
return Number.isSafeInteger(value)
|
|
17
|
+
? { intValue: value }
|
|
18
18
|
: { doubleValue: value };
|
|
19
19
|
}
|
|
20
20
|
if (Array.isArray(value)) {
|
|
@@ -86,6 +86,8 @@ export class BaseServerAdapter extends EventEmitter {
|
|
|
86
86
|
enabled: config.bodyParser?.enabled ?? true,
|
|
87
87
|
maxSize: config.bodyParser?.maxSize ?? "10mb",
|
|
88
88
|
jsonLimit: config.bodyParser?.jsonLimit ?? "10mb",
|
|
89
|
+
// An operator's explicit maxSize also caps raw uploads.
|
|
90
|
+
rawMaxSize: config.bodyParser?.rawMaxSize ?? config.bodyParser?.maxSize ?? "64mb",
|
|
89
91
|
urlEncoded: config.bodyParser?.urlEncoded ?? true,
|
|
90
92
|
},
|
|
91
93
|
logging: {
|
|
@@ -46,7 +46,7 @@ export class ExpressServerAdapter extends BaseServerAdapter {
|
|
|
46
46
|
// Uploads (multipart, octet-stream, audio/*) reach the route as bytes.
|
|
47
47
|
this.app.use(express.raw({
|
|
48
48
|
type: (req) => isRawBodyContentType(req.headers["content-type"]),
|
|
49
|
-
limit: this.config.bodyParser.
|
|
49
|
+
limit: this.config.bodyParser.rawMaxSize || "64mb",
|
|
50
50
|
}));
|
|
51
51
|
}
|
|
52
52
|
// CORS
|
|
@@ -50,7 +50,10 @@ export class FastifyServerAdapter extends BaseServerAdapter {
|
|
|
50
50
|
// Uploads (multipart, octet-stream, audio/*) reach the route as bytes;
|
|
51
51
|
// without a parser Fastify refuses these content types with a 415.
|
|
52
52
|
if (this.config.bodyParser.enabled) {
|
|
53
|
-
this.app.addContentTypeParser(/^\s*(multipart\/form-data|application\/octet-stream|audio\/)/i, {
|
|
53
|
+
this.app.addContentTypeParser(/^\s*(multipart\/form-data|application\/octet-stream|audio\/)/i, {
|
|
54
|
+
parseAs: "buffer",
|
|
55
|
+
bodyLimit: this.parseBodyLimit(this.config.bodyParser.rawMaxSize),
|
|
56
|
+
}, (request, body, done) => {
|
|
54
57
|
done(null, isRawBodyContentType(request.headers["content-type"])
|
|
55
58
|
? body
|
|
56
59
|
: undefined);
|
|
@@ -693,7 +693,7 @@ export class HonoServerAdapter extends BaseServerAdapter {
|
|
|
693
693
|
// a throw from the middleware path would land in onError, which answers
|
|
694
694
|
// 500. The route answers 413 (or a validation error) from it.
|
|
695
695
|
if (isRawBodyContentType(contentType)) {
|
|
696
|
-
const limit = parseByteLimit(this.config.bodyParser.
|
|
696
|
+
const limit = parseByteLimit(this.config.bodyParser.rawMaxSize);
|
|
697
697
|
if (Number(c.req.header("Content-Length") ?? 0) > limit) {
|
|
698
698
|
return new RawBodyTooLargeError(limit);
|
|
699
699
|
}
|
|
@@ -113,7 +113,7 @@ export class KoaServerAdapter extends BaseServerAdapter {
|
|
|
113
113
|
enableTypes: ["json", "form", "text"],
|
|
114
114
|
}));
|
|
115
115
|
// Uploads (multipart, octet-stream, audio/*) reach the route as bytes.
|
|
116
|
-
const rawLimit = parseByteLimit(this.config.bodyParser.
|
|
116
|
+
const rawLimit = parseByteLimit(this.config.bodyParser.rawMaxSize);
|
|
117
117
|
this.app.use(async (ctx, next) => {
|
|
118
118
|
if (isRawBodyContentType(ctx.get("content-type"))) {
|
|
119
119
|
try {
|
|
@@ -12,12 +12,4 @@
|
|
|
12
12
|
* the server's own configuration.
|
|
13
13
|
*/
|
|
14
14
|
import type { RouteGroup, ServerTranscribeRouteOptions } from "../../types/index.js";
|
|
15
|
-
/**
|
|
16
|
-
* Create the transcription routes.
|
|
17
|
-
*
|
|
18
|
-
* @param basePath - Prefix of the NeuroLink route (default `/api`).
|
|
19
|
-
* @param options - `allowedAudioRoots` (default: `audioPath` refused),
|
|
20
|
-
* `maxAudioBytes` (default 50 MB) and `openaiBasePath` (default `""`, so the
|
|
21
|
-
* OpenAI-compatible route is `/v1/audio/transcriptions`).
|
|
22
|
-
*/
|
|
23
15
|
export declare function createTranscribeRoutes(basePath?: string, options?: ServerTranscribeRouteOptions): RouteGroup;
|