@juspay/neurolink 13.6.5 → 13.8.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.
Files changed (87) hide show
  1. package/CHANGELOG.md +6 -2
  2. package/dist/auth/sessionManager.js +8 -3
  3. package/dist/browser/neurolink.min.js +480 -460
  4. package/dist/cli/commands/serve.js +11 -1
  5. package/dist/cli/commands/transcribe.js +39 -3
  6. package/dist/constants/langfuse.d.ts +2 -0
  7. package/dist/constants/langfuse.js +2 -0
  8. package/dist/core/redisConversationMemoryManager.js +6 -16
  9. package/dist/factories/sttDescriptors.js +7 -0
  10. package/dist/index.d.ts +3 -2
  11. package/dist/index.js +4 -2
  12. package/dist/neurolink.js +33 -1
  13. package/dist/observability/exporters/langfuseExporter.d.ts +4 -0
  14. package/dist/observability/exporters/langfuseExporter.js +29 -8
  15. package/dist/observability/otlpEncoding.js +2 -2
  16. package/dist/server/abstract/baseServerAdapter.js +2 -0
  17. package/dist/server/adapters/expressAdapter.js +1 -1
  18. package/dist/server/adapters/fastifyAdapter.js +4 -1
  19. package/dist/server/adapters/honoAdapter.js +1 -1
  20. package/dist/server/adapters/koaAdapter.js +1 -1
  21. package/dist/server/routes/transcribeRoutes.d.ts +0 -8
  22. package/dist/server/routes/transcribeRoutes.js +130 -3
  23. package/dist/server/utils/validation.d.ts +409 -15
  24. package/dist/server/utils/validation.js +106 -4
  25. package/dist/server/websocket/frameQueue.d.ts +16 -0
  26. package/dist/server/websocket/frameQueue.js +55 -0
  27. package/dist/server/websocket/realtimeTranscription.d.ts +48 -0
  28. package/dist/server/websocket/realtimeTranscription.js +603 -0
  29. package/dist/server/websocket/transcribeWebSocket.d.ts +17 -1
  30. package/dist/server/websocket/transcribeWebSocket.js +263 -64
  31. package/dist/services/server/ai/observability/langfuseSetup.js +6 -1
  32. package/dist/telemetry/attributes.d.ts +1 -1
  33. package/dist/telemetry/attributes.js +84 -8
  34. package/dist/types/cli.d.ts +6 -0
  35. package/dist/types/exporter.d.ts +3 -1
  36. package/dist/types/server.d.ts +304 -2
  37. package/dist/types/stt.d.ts +38 -3
  38. package/dist/types/transcribe.d.ts +401 -5
  39. package/dist/utils/redis.d.ts +17 -0
  40. package/dist/utils/redis.js +31 -0
  41. package/dist/voice/audioLevel.d.ts +14 -0
  42. package/dist/voice/audioLevel.js +60 -0
  43. package/dist/voice/codeMix.d.ts +40 -0
  44. package/dist/voice/codeMix.js +282 -0
  45. package/dist/voice/correction/align.d.ts +12 -0
  46. package/dist/voice/correction/align.js +48 -0
  47. package/dist/voice/correction/guards.d.ts +39 -0
  48. package/dist/voice/correction/guards.js +122 -0
  49. package/dist/voice/correction/index.d.ts +25 -3
  50. package/dist/voice/correction/index.js +132 -4
  51. package/dist/voice/correction/rewrite.d.ts +64 -3
  52. package/dist/voice/correction/rewrite.js +295 -25
  53. package/dist/voice/eval/sttEvalLangfuse.d.ts +14 -0
  54. package/dist/voice/eval/sttEvalLangfuse.js +182 -0
  55. package/dist/voice/eval/sttEvalStore.d.ts +27 -0
  56. package/dist/voice/eval/sttEvalStore.js +397 -0
  57. package/dist/voice/index.js +3 -0
  58. package/dist/voice/languages.d.ts +34 -0
  59. package/dist/voice/languages.js +290 -0
  60. package/dist/voice/modelAssets.d.ts +28 -0
  61. package/dist/voice/modelAssets.js +113 -0
  62. package/dist/voice/output/render.d.ts +32 -0
  63. package/dist/voice/output/render.js +368 -0
  64. package/dist/voice/providers/OpenAISTT.d.ts +11 -1
  65. package/dist/voice/providers/OpenAISTT.js +57 -6
  66. package/dist/voice/session/transcriptionSession.d.ts +55 -0
  67. package/dist/voice/session/transcriptionSession.js +225 -0
  68. package/dist/voice/streaming/chunkedStreamAdapter.d.ts +7 -0
  69. package/dist/voice/streaming/chunkedStreamAdapter.js +711 -28
  70. package/dist/voice/streaming/endpointer.d.ts +50 -17
  71. package/dist/voice/streaming/endpointer.js +115 -19
  72. package/dist/voice/transcribe.d.ts +18 -1
  73. package/dist/voice/transcribe.js +459 -18
  74. package/dist/voice/transcribeStream.d.ts +18 -1
  75. package/dist/voice/transcribeStream.js +413 -16
  76. package/dist/voice/vad/assets.d.ts +36 -0
  77. package/dist/voice/vad/assets.js +142 -0
  78. package/dist/voice/vad/frameScorer.d.ts +28 -0
  79. package/dist/voice/vad/frameScorer.js +90 -0
  80. package/dist/voice/vad/sileroVad.d.ts +43 -0
  81. package/dist/voice/vad/sileroVad.js +178 -0
  82. package/dist/voice/whistle/assets.d.ts +3 -4
  83. package/dist/voice/whistle/assets.js +11 -95
  84. package/docs-site/static/search-index.json +21 -6
  85. package/models/silero-vad/README.md +19 -0
  86. package/models/silero-vad/silero_vad.onnx +0 -0
  87. package/package.json +4 -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: fileConfig.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 = { provider: argv.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: "Second STT provider run on the same audio; the rewrite reconciles both",
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: { sampleRate: TARGET_SAMPLE_RATE },
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. */
@@ -16,7 +16,7 @@ import { buildContextFromPointer, getEffectiveTokenThreshold, } from "../utils/c
16
16
  import { logger } from "../utils/logger.js";
17
17
  import { assistantTurnIdentity, isTurnAlreadyStored, turnMessageFields, userTurnIdentity, } from "../utils/conversationTurnIdentity.js";
18
18
  import { extractIsErrorText } from "../utils/toolResultStatus.js";
19
- import { deserializeConversation, encodeStoredMessages, getSessionMessagesKey, MESSAGES_KEY_SUFFIX, isSessionMessagesKey, parseStoredMessages, serializeConversationMetadata, usesSplitMessageStorage, getNormalizedConfig, getPooledRedisClient, getSessionKey, getUserSessionsKey, releasePooledRedisClient, scanKeys, serializeConversation, } from "../utils/redis.js";
19
+ import { addToSetWithTtl, deserializeConversation, encodeStoredMessages, getSessionMessagesKey, MESSAGES_KEY_SUFFIX, isSessionMessagesKey, parseStoredMessages, serializeConversationMetadata, usesSplitMessageStorage, getNormalizedConfig, getPooledRedisClient, getSessionKey, getUserSessionsKey, releasePooledRedisClient, scanKeys, serializeConversation, setWithTtl, } from "../utils/redis.js";
20
20
  const redisTracer = tracers.redis;
21
21
  const REDIS_TIMEOUT_MS = 5000;
22
22
  /**
@@ -265,10 +265,7 @@ export class RedisConversationMemoryManager {
265
265
  }
266
266
  try {
267
267
  const userSessionsKey = getUserSessionsKey(this.redisConfig, userId);
268
- await this.redisClient.sAdd(userSessionsKey, sessionId);
269
- if (this.redisConfig.ttl > 0) {
270
- await this.redisClient.expire(userSessionsKey, this.redisConfig.ttl);
271
- }
268
+ await addToSetWithTtl(this.redisClient, userSessionsKey, sessionId, this.redisConfig.ttl);
272
269
  }
273
270
  catch (error) {
274
271
  logger.error("[RedisConversationMemoryManager] Failed to add session to user set", {
@@ -420,9 +417,8 @@ export class RedisConversationMemoryManager {
420
417
  updatedConversation.title = title;
421
418
  updatedConversation.updatedAt = new Date().toISOString();
422
419
  const serializedData = serializeConversation(updatedConversation);
423
- await this.redisClient?.set(updatedRedisKey, serializedData);
424
- if (this.redisConfig.ttl > 0) {
425
- await this.redisClient?.expire(updatedRedisKey, this.redisConfig.ttl);
420
+ if (this.redisClient) {
421
+ await setWithTtl(this.redisClient, updatedRedisKey, serializedData, this.redisConfig.ttl);
426
422
  }
427
423
  }
428
424
  }
@@ -628,10 +624,7 @@ export class RedisConversationMemoryManager {
628
624
  const freshSerialized = usesSplitMessageStorage(latestConversation)
629
625
  ? serializeConversationMetadata(latestConversation)
630
626
  : serializeConversation(latestConversation);
631
- await this.redisClient.set(redisKey, freshSerialized);
632
- if (this.redisConfig.ttl > 0) {
633
- await this.redisClient.expire(redisKey, this.redisConfig.ttl);
634
- }
627
+ await setWithTtl(this.redisClient, redisKey, freshSerialized, this.redisConfig.ttl);
635
628
  }
636
629
  }
637
630
  }
@@ -1695,10 +1688,7 @@ User message: "${userMessage}"`;
1695
1688
  const serializedData = usesSplitMessageStorage(conversation)
1696
1689
  ? serializeConversationMetadata(conversation)
1697
1690
  : serializeConversation(conversation);
1698
- await withTimeout(this.redisClient.set(redisKey, serializedData), 5000);
1699
- if (this.redisConfig.ttl > 0) {
1700
- await withTimeout(this.redisClient.expire(redisKey, this.redisConfig.ttl), 5000);
1701
- }
1691
+ await withTimeout(setWithTtl(this.redisClient, redisKey, serializedData, this.redisConfig.ttl), 5000);
1702
1692
  logger.info("[RedisConversationMemoryManager] Successfully updated agentic loop report", {
1703
1693
  sessionId,
1704
1694
  userId,
@@ -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";
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 ?? process.env.LANGFUSE_BASE_URL,
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
  }
@@ -14710,6 +14712,36 @@ Current user's request: ${currentInput}`;
14710
14712
  clearTimeout(timer);
14711
14713
  }
14712
14714
  },
14715
+ render: async (o) => {
14716
+ const controller = new AbortController();
14717
+ const timer = setTimeout(() => controller.abort(), o.timeoutMs);
14718
+ // Output rendering is translation, often worth a stronger model than
14719
+ // the rewrite; it has its own setting and falls back to the rewrite's.
14720
+ const provider = o.provider ??
14721
+ (process.env.NEUROLINK_STT_OUTPUT_PROVIDER?.trim() ||
14722
+ process.env.NEUROLINK_STT_REWRITE_PROVIDER?.trim());
14723
+ const model = o.model ??
14724
+ (process.env.NEUROLINK_STT_OUTPUT_MODEL?.trim() ||
14725
+ process.env.NEUROLINK_STT_REWRITE_MODEL?.trim());
14726
+ try {
14727
+ const generated = await this.generate({
14728
+ input: { text: o.user },
14729
+ systemPrompt: o.system,
14730
+ schema: o.schema,
14731
+ ...(provider ? { provider } : {}),
14732
+ ...(model ? { model } : {}),
14733
+ maxTokens: o.maxTokens,
14734
+ temperature: 0,
14735
+ disableTools: true,
14736
+ memory: { enabled: false },
14737
+ abortSignal: controller.signal,
14738
+ });
14739
+ return generated.structuredData ?? generated.content ?? "";
14740
+ }
14741
+ finally {
14742
+ clearTimeout(timer);
14743
+ }
14744
+ },
14713
14745
  };
14714
14746
  }
14715
14747
  /**
@@ -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 ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
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 > 0) {
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
- if (!credentialsWarned) {
338
- credentialsWarned = true;
339
- logger.warn("[langfuse] Langfuse credentials rejected");
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.isInteger(value)
17
- ? { intValue: String(value) }
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.maxSize || "10mb",
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, { parseAs: "buffer" }, (request, body, done) => {
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.maxSize);
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.maxSize);
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;