@juspay/neurolink 13.8.17 → 13.8.19
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 +5 -2
- package/README.md +1 -1
- package/dist/action/actionExecutor.d.ts +33 -3
- package/dist/action/actionExecutor.js +71 -15
- package/dist/action/actionInputs.js +4 -1
- package/dist/action/cliVersion.d.ts +25 -0
- package/dist/action/cliVersion.js +26 -0
- package/dist/action/index.d.ts +2 -1
- package/dist/action/index.js +2 -1
- package/dist/adapters/video/directorPipeline.js +46 -36
- package/dist/adapters/video/ffmpegAdapter.d.ts +17 -0
- package/dist/adapters/video/ffmpegAdapter.js +32 -5
- package/dist/adapters/video/frameExtractor.js +1 -1
- package/dist/adapters/video/videoMerger.js +5 -2
- package/dist/agent/arithmeticEvaluator.d.ts +32 -0
- package/dist/agent/arithmeticEvaluator.js +349 -0
- package/dist/agent/directTools.d.ts +6 -18
- package/dist/agent/directTools.js +17 -56
- package/dist/artifacts/artifactStore.d.ts +22 -1
- package/dist/artifacts/artifactStore.js +66 -6
- package/dist/auth/anthropicOAuth.d.ts +26 -1
- package/dist/auth/anthropicOAuth.js +81 -5
- package/dist/auth/secureFiles.d.ts +37 -0
- package/dist/auth/secureFiles.js +95 -0
- package/dist/auth/tokenStore.d.ts +43 -6
- package/dist/auth/tokenStore.js +260 -79
- package/dist/avatar/providers/DIDAvatar.js +49 -45
- package/dist/avatar/providers/ReplicateAvatar.js +33 -40
- package/dist/browser/neurolink.min.js +485 -558
- package/dist/cli/commands/auth.js +112 -70
- package/dist/cli/commands/proxy.d.ts +4 -0
- package/dist/cli/commands/proxy.js +128 -13
- package/dist/cli/commands/serve.d.ts +10 -1
- package/dist/cli/commands/serve.js +98 -17
- package/dist/cli/commands/server.js +17 -5
- package/dist/cli/index.js +146 -5
- package/dist/cli/loop/optionsSchema.d.ts +1 -1
- package/dist/cli/utils/envManager.js +17 -3
- package/dist/cli/utils/serverUtils.d.ts +39 -3
- package/dist/cli/utils/serverUtils.js +63 -6
- package/dist/core/baseProvider.js +7 -1
- package/dist/core/modules/MessageBuilder.js +5 -0
- package/dist/core/modules/ToolsManager.d.ts +46 -1
- package/dist/core/modules/ToolsManager.js +190 -43
- package/dist/files/fileReferenceRegistry.d.ts +2 -23
- package/dist/files/fileReferenceRegistry.js +55 -5
- package/dist/hitl/hitlManager.js +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +6 -1
- package/dist/mcp/auth/tokenStorage.d.ts +7 -0
- package/dist/mcp/auth/tokenStorage.js +23 -4
- package/dist/mcp/externalServerManager.d.ts +13 -3
- package/dist/mcp/externalServerManager.js +154 -136
- package/dist/mcp/index.d.ts +2 -0
- package/dist/mcp/index.js +3 -0
- package/dist/mcp/mcpClientFactory.d.ts +14 -0
- package/dist/mcp/mcpClientFactory.js +70 -7
- package/dist/mcp/projectConfigTrust.d.ts +72 -0
- package/dist/mcp/projectConfigTrust.js +237 -0
- package/dist/mcp/transportUrlPolicy.d.ts +39 -0
- package/dist/mcp/transportUrlPolicy.js +172 -0
- package/dist/music/providers/BeatovenMusic.js +35 -46
- package/dist/music/providers/ReplicateMusic.js +33 -40
- package/dist/neurolink.d.ts +16 -1
- package/dist/neurolink.js +64 -11
- package/dist/processors/archive/ArchiveProcessor.js +15 -1
- package/dist/processors/archive/zipEntryIndex.d.ts +22 -0
- package/dist/processors/archive/zipEntryIndex.js +33 -0
- package/dist/processors/base/BaseFileProcessor.d.ts +9 -21
- package/dist/processors/base/BaseFileProcessor.js +40 -111
- package/dist/processors/data/YamlProcessor.d.ts +13 -1
- package/dist/processors/data/YamlProcessor.js +93 -2
- package/dist/processors/document/OpenDocumentProcessor.js +7 -2
- package/dist/processors/document/PptxProcessor.js +12 -7
- package/dist/processors/media/VideoProcessor.js +93 -15
- package/dist/providers/openaiChatCompletionsClient.js +12 -3
- package/dist/proxy/accountLedger.js +6 -8
- package/dist/proxy/geminiFormat.d.ts +3 -11
- package/dist/proxy/geminiFormat.js +11 -2
- package/dist/proxy/inboundAuth.d.ts +89 -0
- package/dist/proxy/inboundAuth.js +219 -0
- package/dist/proxy/openaiFormat.d.ts +3 -9
- package/dist/proxy/openaiFormat.js +21 -15
- package/dist/proxy/proxyTranslationEngine.d.ts +2 -6
- package/dist/proxy/proxyTranslationEngine.js +25 -14
- package/dist/proxy/routingPolicy.d.ts +3 -1
- package/dist/proxy/routingPolicy.js +7 -8
- package/dist/proxy/tokenRefresh.d.ts +6 -0
- package/dist/proxy/tokenRefresh.js +13 -1
- package/dist/proxy/updateState.js +4 -4
- package/dist/rag/document/loaders.d.ts +0 -6
- package/dist/rag/document/loaders.js +16 -10
- package/dist/rag/ragIntegration.d.ts +2 -2
- package/dist/rag/ragIntegration.js +35 -10
- package/dist/rag/retrieval/vectorQueryTool.d.ts +7 -0
- package/dist/rag/retrieval/vectorQueryTool.js +54 -17
- package/dist/server/abstract/baseServerAdapter.d.ts +43 -0
- package/dist/server/abstract/baseServerAdapter.js +119 -6
- package/dist/server/adapters/expressAdapter.d.ts +3 -0
- package/dist/server/adapters/expressAdapter.js +78 -6
- package/dist/server/adapters/fastifyAdapter.d.ts +3 -0
- package/dist/server/adapters/fastifyAdapter.js +165 -70
- package/dist/server/adapters/honoAdapter.d.ts +9 -1
- package/dist/server/adapters/honoAdapter.js +99 -21
- package/dist/server/adapters/koaAdapter.d.ts +1 -0
- package/dist/server/adapters/koaAdapter.js +153 -74
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.js +4 -0
- package/dist/server/middleware/auth.js +6 -2
- package/dist/server/middleware/cache.js +6 -1
- package/dist/server/middleware/rateLimit.js +29 -4
- package/dist/server/routes/agentRoutes.js +24 -11
- package/dist/server/routes/claudeProxyRoutes.d.ts +1 -0
- package/dist/server/routes/claudeProxyRoutes.js +273 -84
- package/dist/server/routes/codexProxyRoutes.d.ts +2 -2
- package/dist/server/routes/codexProxyRoutes.js +125 -40
- package/dist/server/routes/index.d.ts +4 -0
- package/dist/server/routes/index.js +35 -4
- package/dist/server/routes/mcpRoutes.d.ts +6 -2
- package/dist/server/routes/mcpRoutes.js +21 -7
- package/dist/server/routes/memoryRoutes.js +7 -6
- package/dist/server/routes/toolRoutes.d.ts +12 -2
- package/dist/server/routes/toolRoutes.js +33 -3
- package/dist/server/routes/transcribeRoutes.js +60 -13
- package/dist/server/utils/clientAddress.d.ts +23 -0
- package/dist/server/utils/clientAddress.js +65 -0
- package/dist/server/utils/clientError.d.ts +35 -0
- package/dist/server/utils/clientError.js +129 -0
- package/dist/server/utils/pathMatch.d.ts +21 -0
- package/dist/server/utils/pathMatch.js +41 -0
- package/dist/server/utils/validation.d.ts +9 -0
- package/dist/server/utils/validation.js +9 -0
- package/dist/server/websocket/WebSocketHandler.js +22 -3
- package/dist/server/websocket/frameQueue.d.ts +19 -0
- package/dist/server/websocket/frameQueue.js +41 -3
- package/dist/server/websocket/transcribeWebSocket.d.ts +8 -1
- package/dist/server/websocket/transcribeWebSocket.js +223 -34
- package/dist/skills/skillStores.js +5 -0
- package/dist/tasks/store/fileTaskStore.d.ts +2 -0
- package/dist/tasks/store/fileTaskStore.js +11 -4
- package/dist/types/attachments.d.ts +117 -0
- package/dist/types/attachments.js +15 -0
- package/dist/types/auth.d.ts +6 -0
- package/dist/types/cli.d.ts +4 -0
- package/dist/types/config.d.ts +56 -0
- package/dist/types/file.d.ts +8 -0
- package/dist/types/hitl.d.ts +8 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -0
- package/dist/types/mcp.d.ts +98 -0
- package/dist/types/multimodal.js +0 -41
- package/dist/types/openaiCompatible.d.ts +1 -1
- package/dist/types/processor.d.ts +14 -1
- package/dist/types/proxy.d.ts +71 -16
- package/dist/types/proxyClient.d.ts +2 -0
- package/dist/types/rag.d.ts +9 -0
- package/dist/types/safeFetch.d.ts +40 -0
- package/dist/types/server.d.ts +206 -10
- package/dist/types/tools.d.ts +57 -0
- package/dist/types/transcribe.d.ts +6 -0
- package/dist/utils/attachmentPolicy.d.ts +89 -0
- package/dist/utils/attachmentPolicy.js +327 -0
- package/dist/utils/dotenvBootstrap.d.ts +48 -19
- package/dist/utils/dotenvBootstrap.js +172 -31
- package/dist/utils/fileDetector.js +115 -65
- package/dist/utils/json/coerce.js +11 -1
- package/dist/utils/logSanitize.d.ts +5 -0
- package/dist/utils/logSanitize.js +8 -0
- package/dist/utils/logger.js +73 -1
- package/dist/utils/messageBuilder.d.ts +2 -2
- package/dist/utils/messageBuilder.js +118 -105
- package/dist/utils/multimodalOptionsBuilder.d.ts +1 -0
- package/dist/utils/multimodalOptionsBuilder.js +2 -0
- package/dist/utils/redis.d.ts +12 -0
- package/dist/utils/redis.js +72 -0
- package/dist/utils/retryAfter.d.ts +32 -0
- package/dist/utils/retryAfter.js +135 -11
- package/dist/utils/safeFetch.d.ts +22 -1
- package/dist/utils/safeFetch.js +132 -38
- package/dist/utils/safeStoreId.d.ts +19 -0
- package/dist/utils/safeStoreId.js +29 -0
- package/dist/utils/schemaConversion.d.ts +30 -3
- package/dist/utils/schemaConversion.js +457 -84
- package/dist/utils/sizeGuard.js +2 -2
- package/dist/utils/ssrfGuard.d.ts +3 -3
- package/dist/utils/ssrfGuard.js +71 -39
- package/dist/voice/eval/sttEvalStore.d.ts +26 -3
- package/dist/voice/eval/sttEvalStore.js +77 -12
- package/dist/voice/livekit/agentDefinition.d.ts +18 -0
- package/dist/voice/livekit/agentDefinition.js +22 -0
- package/dist/voice/livekit/realtimeMcpTools.d.ts +1 -1
- package/dist/voice/livekit/realtimeMcpTools.js +8 -7
- package/dist/voice/livekit/realtimeVoiceAgent.d.ts +2 -4
- package/dist/voice/livekit/realtimeVoiceAgent.js +18 -7
- package/dist/voice/livekit/voiceAgent.d.ts +2 -2
- package/dist/voice/livekit/voiceAgent.js +2 -1
- package/dist/voice/livekit/xaiSessionWatch.d.ts +0 -5
- package/dist/voice/livekit/xaiSessionWatch.js +30 -0
- package/dist/voice/providers/GeminiLive.js +21 -6
- package/dist/voice/providers/GoogleSTT.js +8 -5
- package/dist/voice/transcribe.d.ts +2 -2
- package/dist/voice/transcribe.js +34 -18
- package/dist/voice/whistle/audio.js +11 -5
- package/dist/workflow/workflows/adaptiveWorkflow.d.ts +2 -2
- package/dist/workflow/workflows/adaptiveWorkflow.js +10 -10
- package/dist/workflow/workflows/consensusWorkflow.d.ts +4 -4
- package/dist/workflow/workflows/consensusWorkflow.js +12 -12
- package/dist/workflow/workflows/fallbackWorkflow.d.ts +3 -3
- package/dist/workflow/workflows/fallbackWorkflow.js +7 -7
- package/dist/workflow/workflows/multiJudgeWorkflow.d.ts +7 -7
- package/dist/workflow/workflows/multiJudgeWorkflow.js +31 -31
- package/docs-site/static/search-index.json +74 -52
- package/package.json +173 -156
- package/scripts/observability/import-openobserve-dashboard.mjs +11 -3
|
@@ -17,6 +17,7 @@ import { withSpan } from "../../telemetry/withSpan.js";
|
|
|
17
17
|
import { tracers } from "../../telemetry/tracers.js";
|
|
18
18
|
import { logger } from "../../utils/logger.js";
|
|
19
19
|
import { MAX_AUDIO_BYTES } from "../../utils/sizeGuard.js";
|
|
20
|
+
import { clientFacingError, logClientFacingError, PROVIDER_NOT_CONFIGURED_MESSAGE, } from "../utils/clientError.js";
|
|
20
21
|
import { RawBodyTooLargeError } from "../utils/rawBody.js";
|
|
21
22
|
import { getSTTProviderDescriptor, listConfiguredSTTProviders, resolveDefaultSTTProvider, resolveSTTEndpoints, } from "../../factories/sttDescriptors.js";
|
|
22
23
|
import { STTProcessor } from "../../utils/sttProcessor.js";
|
|
@@ -170,7 +171,15 @@ async function resolveAudio(request, options, limit) {
|
|
|
170
171
|
});
|
|
171
172
|
}
|
|
172
173
|
catch (error) {
|
|
173
|
-
|
|
174
|
+
// The guard's reason can name the address a host resolved to, which
|
|
175
|
+
// would let a caller map the server's network one URL at a time. The
|
|
176
|
+
// caller gets a fixed message; the operator gets the reason in the log,
|
|
177
|
+
// with the URL itself redacted (as agentInput.ts does).
|
|
178
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
179
|
+
logger.warn("[transcribeRoutes] refused an audioUrl", {
|
|
180
|
+
reason: reason.replaceAll(request.audioUrl, "<url>"),
|
|
181
|
+
});
|
|
182
|
+
throw new TranscribeRequestError("audioUrl could not be fetched (it must be a public https URL, answer without a redirect and fit the size limit)", 400, "VALIDATION_ERROR");
|
|
174
183
|
}
|
|
175
184
|
return {
|
|
176
185
|
audio,
|
|
@@ -213,7 +222,14 @@ function toTranscribeOptions(config, audio, format, maxAudioBytes) {
|
|
|
213
222
|
...(config.timeoutMs !== undefined ? { timeoutMs: config.timeoutMs } : {}),
|
|
214
223
|
};
|
|
215
224
|
}
|
|
216
|
-
/**
|
|
225
|
+
/**
|
|
226
|
+
* Status, code and the message the client may see for an error thrown by
|
|
227
|
+
* `transcribe()` or by this file. The exception's own text is passed through
|
|
228
|
+
* only when this file composed it or when it describes the request (an empty
|
|
229
|
+
* or malformed recording); a missing engine is named by category, never by
|
|
230
|
+
* the variable that would configure it, and anything else is reduced to a
|
|
231
|
+
* fixed message. The detail is logged by the caller.
|
|
232
|
+
*/
|
|
217
233
|
function classifyError(error) {
|
|
218
234
|
const message = error instanceof Error ? error.message : String(error);
|
|
219
235
|
if (error instanceof TranscribeRequestError) {
|
|
@@ -222,10 +238,26 @@ function classifyError(error) {
|
|
|
222
238
|
const code = typeof error === "object" && error !== null && "code" in error
|
|
223
239
|
? String(error.code)
|
|
224
240
|
: undefined;
|
|
241
|
+
if (code === "STT_PROVIDER_NOT_CONFIGURED") {
|
|
242
|
+
return {
|
|
243
|
+
status: 503,
|
|
244
|
+
code: "PROVIDER_NOT_CONFIGURED",
|
|
245
|
+
message: PROVIDER_NOT_CONFIGURED_MESSAGE,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
225
248
|
if (code && CLIENT_ERROR_CODES.has(code)) {
|
|
226
249
|
return { status: 400, code: "VALIDATION_ERROR", message };
|
|
227
250
|
}
|
|
228
|
-
|
|
251
|
+
const shaped = clientFacingError(error);
|
|
252
|
+
return shaped.status === 500
|
|
253
|
+
? { ...shaped, code: "EXECUTION_FAILED" }
|
|
254
|
+
: shaped;
|
|
255
|
+
}
|
|
256
|
+
/** `classifyError` plus the server-side log the client-facing message leaves out. */
|
|
257
|
+
function classifyAndLog(ctx, route, error) {
|
|
258
|
+
const classified = classifyError(error);
|
|
259
|
+
logClientFacingError(`transcribeRoutes ${route}`, ctx.requestId, error, classified);
|
|
260
|
+
return classified;
|
|
229
261
|
}
|
|
230
262
|
async function runTranscription(ctx, route, options) {
|
|
231
263
|
return withSpan({
|
|
@@ -420,11 +452,7 @@ async function handleOpenAITranscription(ctx, options, limit, route) {
|
|
|
420
452
|
result = await runTranscription(ctx, route, toTranscribeOptions(parsed.data, audio, format, limit));
|
|
421
453
|
}
|
|
422
454
|
catch (error) {
|
|
423
|
-
const classified =
|
|
424
|
-
logger.warn("[transcribeRoutes] OpenAI-compatible transcription failed", {
|
|
425
|
-
requestId: ctx.requestId,
|
|
426
|
-
error: classified.message,
|
|
427
|
-
});
|
|
455
|
+
const classified = classifyAndLog(ctx, route, error);
|
|
428
456
|
return openAIError(classified.status, classified.message);
|
|
429
457
|
}
|
|
430
458
|
if (responseFormat === "text") {
|
|
@@ -541,7 +569,17 @@ export function createTranscribeRoutes(basePath = "/api", options = {}) {
|
|
|
541
569
|
const nativePath = `${basePath}/agent/transcribe`;
|
|
542
570
|
const openaiPath = `${options.openaiBasePath ?? ""}/v1/audio/transcriptions`;
|
|
543
571
|
const evalPath = `${options.openaiBasePath ?? ""}/v1/evals/stt`;
|
|
544
|
-
|
|
572
|
+
// The collector keeps every transcript it is sent and reads them back to
|
|
573
|
+
// whoever asks, so it is mounted only when the operator asks for it: an
|
|
574
|
+
// options object, or NEUROLINK_STT_EVAL_ENABLED=1. It used to be on by
|
|
575
|
+
// default.
|
|
576
|
+
const evalStore = options.evals === false
|
|
577
|
+
? undefined
|
|
578
|
+
: options.evals !== undefined
|
|
579
|
+
? new STTEvalStore(options.evals)
|
|
580
|
+
: process.env.NEUROLINK_STT_EVAL_ENABLED?.trim() === "1"
|
|
581
|
+
? new STTEvalStore()
|
|
582
|
+
: undefined;
|
|
545
583
|
return {
|
|
546
584
|
// Empty prefix: the two routes live under different roots, and every
|
|
547
585
|
// route path already starts with "", so the adapters add nothing.
|
|
@@ -561,7 +599,7 @@ export function createTranscribeRoutes(basePath = "/api", options = {}) {
|
|
|
561
599
|
return await runTranscription(ctx, nativePath, toTranscribeOptions(request, audio, format, limit));
|
|
562
600
|
}
|
|
563
601
|
catch (error) {
|
|
564
|
-
const classified =
|
|
602
|
+
const classified = classifyAndLog(ctx, nativePath, error);
|
|
565
603
|
return createErrorResponse(classified.code, classified.message, undefined, ctx.requestId, classified.status);
|
|
566
604
|
}
|
|
567
605
|
},
|
|
@@ -593,6 +631,11 @@ export function createTranscribeRoutes(basePath = "/api", options = {}) {
|
|
|
593
631
|
* apart they were per speaker.
|
|
594
632
|
*/
|
|
595
633
|
function createEvalRoutes(evalPath, store) {
|
|
634
|
+
// A person the server identified owns the calls they post and sees only
|
|
635
|
+
// those; a shared key names no person, so its calls are visible to every
|
|
636
|
+
// holder, as before. The handler sees ctx.user only when the server carries
|
|
637
|
+
// middleware state (see ServerAdapterConfig.carryMiddlewareState).
|
|
638
|
+
const ownerOf = (ctx) => ctx.user && ctx.user.shared !== true ? ctx.user.id : undefined;
|
|
596
639
|
return [
|
|
597
640
|
{
|
|
598
641
|
method: "POST",
|
|
@@ -603,7 +646,9 @@ function createEvalRoutes(evalPath, store) {
|
|
|
603
646
|
return validation.error;
|
|
604
647
|
}
|
|
605
648
|
const batch = validation.data;
|
|
606
|
-
|
|
649
|
+
const accepted = await store.accept(batch, ownerOf(ctx));
|
|
650
|
+
return (accepted ??
|
|
651
|
+
createErrorResponse("FORBIDDEN", "This call belongs to another caller", undefined, ctx.requestId, 403));
|
|
607
652
|
},
|
|
608
653
|
description: "Accept a batch of one call's segments from two STTs (primary shown, shadow for evaluation); a resent segment id replaces the earlier one",
|
|
609
654
|
tags: ["transcribe", "evaluation"],
|
|
@@ -611,7 +656,9 @@ function createEvalRoutes(evalPath, store) {
|
|
|
611
656
|
{
|
|
612
657
|
method: "GET",
|
|
613
658
|
path: evalPath,
|
|
614
|
-
handler: async () => ({
|
|
659
|
+
handler: async (ctx) => ({
|
|
660
|
+
calls: await store.list(ownerOf(ctx)),
|
|
661
|
+
}),
|
|
615
662
|
description: "Calls collected for STT evaluation, most recent first",
|
|
616
663
|
tags: ["transcribe", "evaluation"],
|
|
617
664
|
},
|
|
@@ -622,7 +669,7 @@ function createEvalRoutes(evalPath, store) {
|
|
|
622
669
|
// Adapters already decode path params; decoding again would turn a
|
|
623
670
|
// literal "%" in an id into something else, or throw on "%E0%A4%A".
|
|
624
671
|
const callId = ctx.params.callId ?? "";
|
|
625
|
-
const report = await store.report(callId, ctx.query.segments === "true");
|
|
672
|
+
const report = await store.report(callId, ctx.query.segments === "true", ownerOf(ctx));
|
|
626
673
|
return (report ??
|
|
627
674
|
createErrorResponse("NOT_FOUND", `No STT evaluation for call ${callId}`, undefined, ctx.requestId, 404));
|
|
628
675
|
},
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The address a request is counted against.
|
|
3
|
+
*
|
|
4
|
+
* The TCP peer is the only address a client cannot choose, so it is the
|
|
5
|
+
* default. `X-Forwarded-For` is read only when the operator says a reverse
|
|
6
|
+
* proxy stands in front of the server (`trustProxy`), and then from the right:
|
|
7
|
+
* the hops of the listed proxies are skipped and the first other entry is the
|
|
8
|
+
* client, because a proxy appends the address it saw and anything a client put
|
|
9
|
+
* in the header itself sits further left.
|
|
10
|
+
*
|
|
11
|
+
* @module server/utils/clientAddress
|
|
12
|
+
*/
|
|
13
|
+
import type { ServerClientAddressInput } from "../../types/index.js";
|
|
14
|
+
/** Strip an IPv4-mapped IPv6 prefix and a port, so one client has one key. */
|
|
15
|
+
export declare function canonicalAddress(address: string): string | undefined;
|
|
16
|
+
/**
|
|
17
|
+
* The client's address, or `undefined` when the runtime reports no peer and
|
|
18
|
+
* no trusted header names one. The result is always a bare IP, so a header
|
|
19
|
+
* holding anything else cannot become a counter key.
|
|
20
|
+
*/
|
|
21
|
+
export declare function resolveClientAddress(input: ServerClientAddressInput): string | undefined;
|
|
22
|
+
/** `true` for an address only this host can reach: 127/8, `::1`, or their mapped forms. */
|
|
23
|
+
export declare function isLoopbackAddress(address: string | undefined): boolean;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The address a request is counted against.
|
|
3
|
+
*
|
|
4
|
+
* The TCP peer is the only address a client cannot choose, so it is the
|
|
5
|
+
* default. `X-Forwarded-For` is read only when the operator says a reverse
|
|
6
|
+
* proxy stands in front of the server (`trustProxy`), and then from the right:
|
|
7
|
+
* the hops of the listed proxies are skipped and the first other entry is the
|
|
8
|
+
* client, because a proxy appends the address it saw and anything a client put
|
|
9
|
+
* in the header itself sits further left.
|
|
10
|
+
*
|
|
11
|
+
* @module server/utils/clientAddress
|
|
12
|
+
*/
|
|
13
|
+
import { isIP } from "node:net";
|
|
14
|
+
/** Strip an IPv4-mapped IPv6 prefix and a port, so one client has one key. */
|
|
15
|
+
export function canonicalAddress(address) {
|
|
16
|
+
let value = address.trim();
|
|
17
|
+
if (value.startsWith("[")) {
|
|
18
|
+
const end = value.indexOf("]");
|
|
19
|
+
value = end === -1 ? value.slice(1) : value.slice(1, end);
|
|
20
|
+
}
|
|
21
|
+
else if (/^\d{1,3}(\.\d{1,3}){3}:\d+$/.test(value) ||
|
|
22
|
+
/^[^:]+:\d+$/.test(value)) {
|
|
23
|
+
value = value.slice(0, value.lastIndexOf(":"));
|
|
24
|
+
}
|
|
25
|
+
if (value.toLowerCase().startsWith("::ffff:") && isIP(value.slice(7)) === 4) {
|
|
26
|
+
value = value.slice(7);
|
|
27
|
+
}
|
|
28
|
+
return isIP(value) ? value : undefined;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The client's address, or `undefined` when the runtime reports no peer and
|
|
32
|
+
* no trusted header names one. The result is always a bare IP, so a header
|
|
33
|
+
* holding anything else cannot become a counter key.
|
|
34
|
+
*/
|
|
35
|
+
export function resolveClientAddress(input) {
|
|
36
|
+
const peer = input.remoteAddress
|
|
37
|
+
? canonicalAddress(input.remoteAddress)
|
|
38
|
+
: undefined;
|
|
39
|
+
if (!input.trustProxy || !input.forwardedFor) {
|
|
40
|
+
return peer;
|
|
41
|
+
}
|
|
42
|
+
const trusted = new Set(input.trustedProxies
|
|
43
|
+
.map(canonicalAddress)
|
|
44
|
+
.filter((value) => value !== undefined));
|
|
45
|
+
const hops = input.forwardedFor
|
|
46
|
+
.split(",")
|
|
47
|
+
.map(canonicalAddress)
|
|
48
|
+
.filter((value) => value !== undefined);
|
|
49
|
+
for (let index = hops.length - 1; index >= 0; index--) {
|
|
50
|
+
if (!trusted.has(hops[index])) {
|
|
51
|
+
return hops[index];
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return peer;
|
|
55
|
+
}
|
|
56
|
+
/** `true` for an address only this host can reach: 127/8, `::1`, or their mapped forms. */
|
|
57
|
+
export function isLoopbackAddress(address) {
|
|
58
|
+
if (!address) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
const value = canonicalAddress(address) ?? address.trim().toLowerCase();
|
|
62
|
+
return (value === "::1" ||
|
|
63
|
+
value === "localhost" ||
|
|
64
|
+
/^127\.\d+\.\d+\.\d+$/.test(value));
|
|
65
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a failed request may tell the client.
|
|
3
|
+
*
|
|
4
|
+
* An exception's text names environment variables, provider wiring, file
|
|
5
|
+
* paths and upstream response bodies; none of that belongs in a response to
|
|
6
|
+
* an unknown caller. A route therefore answers with one of a few fixed
|
|
7
|
+
* messages plus the request id, and the detail goes to the server log under
|
|
8
|
+
* that id, where the operator can find it. The pattern is the one the OpenAI
|
|
9
|
+
* proxy route already followed.
|
|
10
|
+
*
|
|
11
|
+
* What is passed through: a `ServerAdapterError` below 500, whose message
|
|
12
|
+
* this server composed (a validation failure, a missing route), and the
|
|
13
|
+
* category of a provider that is missing credentials, without saying which
|
|
14
|
+
* credential.
|
|
15
|
+
*
|
|
16
|
+
* @module server/utils/clientError
|
|
17
|
+
*/
|
|
18
|
+
import type { ServerClientFacingError } from "../../types/index.js";
|
|
19
|
+
export declare const GENERIC_ERROR_MESSAGE = "An internal error occurred";
|
|
20
|
+
export declare const PROVIDER_NOT_CONFIGURED_MESSAGE = "The requested provider is not configured on this server";
|
|
21
|
+
export declare const UPSTREAM_RATE_LIMITED_MESSAGE = "The upstream provider is rate limiting this server; retry later";
|
|
22
|
+
export declare const REQUEST_TIMEOUT_MESSAGE = "The request timed out";
|
|
23
|
+
/**
|
|
24
|
+
* The response for `error`. Never the exception's own text unless this
|
|
25
|
+
* server wrote it (a `ServerAdapterError` below 500).
|
|
26
|
+
*/
|
|
27
|
+
export declare function clientFacingError(error: unknown): ServerClientFacingError;
|
|
28
|
+
/**
|
|
29
|
+
* Log the detail the client does not get, under the request id the client
|
|
30
|
+
* does get. A client error is routine traffic and logs as one line; anything
|
|
31
|
+
* else keeps its stack.
|
|
32
|
+
*/
|
|
33
|
+
export declare function logClientFacingError(source: string, requestId: string | undefined, error: unknown, shaped: ServerClientFacingError): void;
|
|
34
|
+
/** {@link clientFacingError} and {@link logClientFacingError} in one step. */
|
|
35
|
+
export declare function shapeRouteError(source: string, requestId: string | undefined, error: unknown): ServerClientFacingError;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a failed request may tell the client.
|
|
3
|
+
*
|
|
4
|
+
* An exception's text names environment variables, provider wiring, file
|
|
5
|
+
* paths and upstream response bodies; none of that belongs in a response to
|
|
6
|
+
* an unknown caller. A route therefore answers with one of a few fixed
|
|
7
|
+
* messages plus the request id, and the detail goes to the server log under
|
|
8
|
+
* that id, where the operator can find it. The pattern is the one the OpenAI
|
|
9
|
+
* proxy route already followed.
|
|
10
|
+
*
|
|
11
|
+
* What is passed through: a `ServerAdapterError` below 500, whose message
|
|
12
|
+
* this server composed (a validation failure, a missing route), and the
|
|
13
|
+
* category of a provider that is missing credentials, without saying which
|
|
14
|
+
* credential.
|
|
15
|
+
*
|
|
16
|
+
* @module server/utils/clientError
|
|
17
|
+
*/
|
|
18
|
+
import { AuthenticationError as ProviderAuthenticationError, RateLimitError as ProviderRateLimitError, } from "../../types/index.js";
|
|
19
|
+
import { logger } from "../../utils/logger.js";
|
|
20
|
+
import { ServerAdapterError } from "../errors.js";
|
|
21
|
+
export const GENERIC_ERROR_MESSAGE = "An internal error occurred";
|
|
22
|
+
export const PROVIDER_NOT_CONFIGURED_MESSAGE = "The requested provider is not configured on this server";
|
|
23
|
+
export const UPSTREAM_RATE_LIMITED_MESSAGE = "The upstream provider is rate limiting this server; retry later";
|
|
24
|
+
export const REQUEST_TIMEOUT_MESSAGE = "The request timed out";
|
|
25
|
+
/** Signs that a provider refused the server's own credentials, or has none. */
|
|
26
|
+
const PROVIDER_CONFIGURATION_PATTERN = /api[ _-]?key|credential|not configured|no (?:ai )?provider|authenticat|unauthori[sz]ed|forbidden|permission denied|invalid[ _-]?(?:key|token)|missing (?:required )?(?:environment|env|key|token)|environment variable/i;
|
|
27
|
+
/** Names the SDK's own error classes and codes use for the same thing. */
|
|
28
|
+
const PROVIDER_CONFIGURATION_CODES = new Set([
|
|
29
|
+
"AUTHENTICATION_ERROR",
|
|
30
|
+
"AUTHENTICATION_FAILED",
|
|
31
|
+
"AUTH_ERROR",
|
|
32
|
+
"INVALID_API_KEY",
|
|
33
|
+
"MISSING_API_KEY",
|
|
34
|
+
"PROVIDER_NOT_CONFIGURED",
|
|
35
|
+
"CONFIGURATION_ERROR",
|
|
36
|
+
"NO_PROVIDER",
|
|
37
|
+
"STT_PROVIDER_NOT_CONFIGURED",
|
|
38
|
+
]);
|
|
39
|
+
function codeOf(error) {
|
|
40
|
+
if (typeof error !== "object" || error === null) {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
const code = error.code;
|
|
44
|
+
return typeof code === "string" ? code : undefined;
|
|
45
|
+
}
|
|
46
|
+
function isAbortError(error) {
|
|
47
|
+
return (error instanceof Error &&
|
|
48
|
+
(error.name === "AbortError" || error.name === "TimeoutError"));
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The response for `error`. Never the exception's own text unless this
|
|
52
|
+
* server wrote it (a `ServerAdapterError` below 500).
|
|
53
|
+
*/
|
|
54
|
+
export function clientFacingError(error) {
|
|
55
|
+
if (error instanceof ServerAdapterError) {
|
|
56
|
+
// The request timeout is the server's deadline, not the client's slowness
|
|
57
|
+
// (the TIMEOUT code maps to 408 for the latter), so it answers 504.
|
|
58
|
+
if (error.name === "ServerTimeoutError") {
|
|
59
|
+
return {
|
|
60
|
+
code: "REQUEST_TIMEOUT",
|
|
61
|
+
message: REQUEST_TIMEOUT_MESSAGE,
|
|
62
|
+
status: 504,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
const status = error.getHttpStatus();
|
|
66
|
+
if (status < 500) {
|
|
67
|
+
return { code: error.code, message: error.message, status };
|
|
68
|
+
}
|
|
69
|
+
return { code: "INTERNAL_ERROR", message: GENERIC_ERROR_MESSAGE, status };
|
|
70
|
+
}
|
|
71
|
+
if (isAbortError(error)) {
|
|
72
|
+
return {
|
|
73
|
+
code: "REQUEST_TIMEOUT",
|
|
74
|
+
message: REQUEST_TIMEOUT_MESSAGE,
|
|
75
|
+
status: 504,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
if (error instanceof ProviderRateLimitError) {
|
|
79
|
+
return {
|
|
80
|
+
code: "UPSTREAM_RATE_LIMITED",
|
|
81
|
+
message: UPSTREAM_RATE_LIMITED_MESSAGE,
|
|
82
|
+
status: 503,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
const code = codeOf(error);
|
|
86
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
87
|
+
if (error instanceof ProviderAuthenticationError ||
|
|
88
|
+
(code !== undefined && PROVIDER_CONFIGURATION_CODES.has(code)) ||
|
|
89
|
+
PROVIDER_CONFIGURATION_PATTERN.test(message)) {
|
|
90
|
+
return {
|
|
91
|
+
code: "PROVIDER_NOT_CONFIGURED",
|
|
92
|
+
message: PROVIDER_NOT_CONFIGURED_MESSAGE,
|
|
93
|
+
status: 503,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
code: "INTERNAL_ERROR",
|
|
98
|
+
message: GENERIC_ERROR_MESSAGE,
|
|
99
|
+
status: 500,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Log the detail the client does not get, under the request id the client
|
|
104
|
+
* does get. A client error is routine traffic and logs as one line; anything
|
|
105
|
+
* else keeps its stack.
|
|
106
|
+
*/
|
|
107
|
+
export function logClientFacingError(source, requestId, error, shaped) {
|
|
108
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
109
|
+
if (shaped.status < 500) {
|
|
110
|
+
logger.warn(`[${source}] Request rejected`, {
|
|
111
|
+
requestId,
|
|
112
|
+
code: shaped.code,
|
|
113
|
+
error: detail,
|
|
114
|
+
});
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
logger.error(`[${source}] Request failed`, {
|
|
118
|
+
requestId,
|
|
119
|
+
code: shaped.code,
|
|
120
|
+
error: detail,
|
|
121
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
/** {@link clientFacingError} and {@link logClientFacingError} in one step. */
|
|
125
|
+
export function shapeRouteError(source, requestId, error) {
|
|
126
|
+
const shaped = clientFacingError(error);
|
|
127
|
+
logClientFacingError(source, requestId, error, shaped);
|
|
128
|
+
return shaped;
|
|
129
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path matching shared by the adapters' middleware `paths` / `excludePaths`
|
|
3
|
+
* and the rate limiters' `skipPaths`.
|
|
4
|
+
*
|
|
5
|
+
* Every match is at a segment boundary: a pattern names the path itself and
|
|
6
|
+
* what lies under it as a directory would, so `/api/health` covers
|
|
7
|
+
* `/api/health` and `/api/health/live` but not `/api/healthz`. The plain
|
|
8
|
+
* prefix test the adapters used to run let the second be exempted by the
|
|
9
|
+
* first.
|
|
10
|
+
*
|
|
11
|
+
* @module server/utils/pathMatch
|
|
12
|
+
*/
|
|
13
|
+
/** The request path without its query string or fragment. */
|
|
14
|
+
export declare function stripQuery(path: string): string;
|
|
15
|
+
/**
|
|
16
|
+
* `true` when `path` is `prefix`, or lies under `prefix` at a segment
|
|
17
|
+
* boundary. `"*"`, `""` and `"/"` match every path.
|
|
18
|
+
*/
|
|
19
|
+
export declare function pathStartsWithSegment(path: string, prefix: string): boolean;
|
|
20
|
+
/** `true` when any of `prefixes` matches `path` (see {@link pathStartsWithSegment}). */
|
|
21
|
+
export declare function pathMatchesAny(path: string, prefixes: readonly string[] | undefined): boolean;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path matching shared by the adapters' middleware `paths` / `excludePaths`
|
|
3
|
+
* and the rate limiters' `skipPaths`.
|
|
4
|
+
*
|
|
5
|
+
* Every match is at a segment boundary: a pattern names the path itself and
|
|
6
|
+
* what lies under it as a directory would, so `/api/health` covers
|
|
7
|
+
* `/api/health` and `/api/health/live` but not `/api/healthz`. The plain
|
|
8
|
+
* prefix test the adapters used to run let the second be exempted by the
|
|
9
|
+
* first.
|
|
10
|
+
*
|
|
11
|
+
* @module server/utils/pathMatch
|
|
12
|
+
*/
|
|
13
|
+
/** The request path without its query string or fragment. */
|
|
14
|
+
export function stripQuery(path) {
|
|
15
|
+
const cut = path.search(/[?#]/);
|
|
16
|
+
return cut === -1 ? path : path.slice(0, cut);
|
|
17
|
+
}
|
|
18
|
+
/** Collapse repeated slashes and drop a trailing one, keeping the root `/`. */
|
|
19
|
+
function normalize(path) {
|
|
20
|
+
const collapsed = path.replace(/\/{2,}/g, "/");
|
|
21
|
+
return collapsed.length > 1 ? collapsed.replace(/\/+$/, "") : collapsed;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* `true` when `path` is `prefix`, or lies under `prefix` at a segment
|
|
25
|
+
* boundary. `"*"`, `""` and `"/"` match every path.
|
|
26
|
+
*/
|
|
27
|
+
export function pathStartsWithSegment(path, prefix) {
|
|
28
|
+
if (prefix === "*" || prefix === "" || prefix === "/") {
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
const target = normalize(stripQuery(path));
|
|
32
|
+
const base = normalize(prefix);
|
|
33
|
+
return target === base || target.startsWith(`${base}/`);
|
|
34
|
+
}
|
|
35
|
+
/** `true` when any of `prefixes` matches `path` (see {@link pathStartsWithSegment}). */
|
|
36
|
+
export function pathMatchesAny(path, prefixes) {
|
|
37
|
+
if (!prefixes || prefixes.length === 0) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
return prefixes.some((prefix) => pathStartsWithSegment(path, prefix));
|
|
41
|
+
}
|
|
@@ -57,6 +57,15 @@ export declare const WebSocketAgentRequestSchema: z.ZodObject<{
|
|
|
57
57
|
maxSteps: z.ZodOptional<z.ZodNumber>;
|
|
58
58
|
}, z.core.$strip>>;
|
|
59
59
|
}, z.core.$strip>;
|
|
60
|
+
/**
|
|
61
|
+
* WebSocket `tool_call` message payload schema: a tool name the registry can
|
|
62
|
+
* look up and a JSON object of arguments. The payload used to reach
|
|
63
|
+
* `executeTool` unchecked.
|
|
64
|
+
*/
|
|
65
|
+
export declare const WebSocketToolCallSchema: z.ZodObject<{
|
|
66
|
+
toolName: z.ZodString;
|
|
67
|
+
args: z.ZodDefault<z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
|
|
68
|
+
}, z.core.$strip>;
|
|
60
69
|
/**
|
|
61
70
|
* Tool execute request schema
|
|
62
71
|
*/
|
|
@@ -55,6 +55,15 @@ export const WebSocketAgentRequestSchema = z.object({
|
|
|
55
55
|
prompt: z.string().min(1, "Prompt is required"),
|
|
56
56
|
options: WebSocketAgentOptionsSchema.optional(),
|
|
57
57
|
});
|
|
58
|
+
/**
|
|
59
|
+
* WebSocket `tool_call` message payload schema: a tool name the registry can
|
|
60
|
+
* look up and a JSON object of arguments. The payload used to reach
|
|
61
|
+
* `executeTool` unchecked.
|
|
62
|
+
*/
|
|
63
|
+
export const WebSocketToolCallSchema = z.object({
|
|
64
|
+
toolName: z.string().min(1, "toolName is required").max(256),
|
|
65
|
+
args: z.record(z.string(), z.unknown()).optional().default({}),
|
|
66
|
+
});
|
|
58
67
|
/**
|
|
59
68
|
* Tool execute request schema
|
|
60
69
|
*/
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { WebSocketError, WebSocketConnectionError } from "../errors.js";
|
|
8
8
|
import { logger } from "../../utils/logger.js";
|
|
9
|
-
import { WebSocketAgentRequestSchema } from "../utils/validation.js";
|
|
9
|
+
import { WebSocketAgentRequestSchema, WebSocketToolCallSchema, } from "../utils/validation.js";
|
|
10
10
|
/**
|
|
11
11
|
* Default WebSocket configuration
|
|
12
12
|
*/
|
|
@@ -36,7 +36,20 @@ export class WebSocketConnectionManager {
|
|
|
36
36
|
pingIntervals = new Map();
|
|
37
37
|
handlers = new Map();
|
|
38
38
|
constructor(config = {}) {
|
|
39
|
-
|
|
39
|
+
// A strategy other than "none" means the deployer wants callers
|
|
40
|
+
// identified, so `required` defaults to true with it: a config that named
|
|
41
|
+
// a strategy and forgot `required` used to get an open socket.
|
|
42
|
+
const strategy = config.auth?.strategy ?? DEFAULT_CONFIG.auth.strategy;
|
|
43
|
+
this.config = {
|
|
44
|
+
...DEFAULT_CONFIG,
|
|
45
|
+
...config,
|
|
46
|
+
auth: {
|
|
47
|
+
...DEFAULT_CONFIG.auth,
|
|
48
|
+
...config.auth,
|
|
49
|
+
strategy,
|
|
50
|
+
required: config.auth?.required ?? strategy !== "none",
|
|
51
|
+
},
|
|
52
|
+
};
|
|
40
53
|
}
|
|
41
54
|
/**
|
|
42
55
|
* Register a handler for a path
|
|
@@ -498,7 +511,13 @@ export function createAgentWebSocketHandler(neurolink) {
|
|
|
498
511
|
return clientPresent ? { type: "stream_complete" } : undefined;
|
|
499
512
|
});
|
|
500
513
|
router.route("tool_call", async (connection, payload) => {
|
|
501
|
-
const
|
|
514
|
+
const parsed = WebSocketToolCallSchema.safeParse(payload);
|
|
515
|
+
if (!parsed.success) {
|
|
516
|
+
throw new WebSocketError(`Invalid tool_call payload: ${parsed.error.issues
|
|
517
|
+
.map((issue) => `${issue.path.join(".") || "payload"} ${issue.message}`)
|
|
518
|
+
.join("; ")}`, undefined, connection.id);
|
|
519
|
+
}
|
|
520
|
+
const { toolName, args } = parsed.data;
|
|
502
521
|
const result = await neurolink.executeTool(toolName, args);
|
|
503
522
|
return { type: "tool_result", toolName, result };
|
|
504
523
|
});
|
|
@@ -2,15 +2,34 @@
|
|
|
2
2
|
* Push-driven async iterable shared by the transcription sockets: socket
|
|
3
3
|
* frames are pushed in, `transcribeStream()` pulls them out.
|
|
4
4
|
*
|
|
5
|
+
* The queue reports its level to whoever feeds it, so a socket can be paused
|
|
6
|
+
* when audio arrives faster than the engine takes it and resumed once the
|
|
7
|
+
* backlog drains (TCP backpressure, instead of buffering the whole burst).
|
|
8
|
+
* Every queue also charges its bytes to one process-wide budget, so a burst
|
|
9
|
+
* of uploads across many sockets cannot exhaust the process.
|
|
10
|
+
*
|
|
5
11
|
* @module server/websocket/frameQueue
|
|
6
12
|
*/
|
|
13
|
+
/** What every queue in the process holds right now, for caps and tests. */
|
|
14
|
+
export declare function totalQueuedFrameBytes(): number;
|
|
7
15
|
/** Push-driven async iterable: socket frames in, `transcribeStream()` pulls. */
|
|
8
16
|
export declare class FrameQueue implements AsyncIterable<Buffer> {
|
|
9
17
|
private readonly frames;
|
|
10
18
|
private waiting;
|
|
11
19
|
private ended;
|
|
20
|
+
private onLevel;
|
|
12
21
|
queuedBytes: number;
|
|
22
|
+
/** Called after every change of `queuedBytes`, with the new level. */
|
|
23
|
+
setLevelListener(listener: ((queuedBytes: number) => void) | undefined): void;
|
|
13
24
|
push(frame: Buffer): void;
|
|
25
|
+
/**
|
|
26
|
+
* No more frames will arrive; the ones already queued are still handed
|
|
27
|
+
* out, so a recording whose `end` frame followed its audio is transcribed
|
|
28
|
+
* whole. The consumer's `return()` (see below) is what discards.
|
|
29
|
+
*/
|
|
14
30
|
end(): void;
|
|
31
|
+
/** The consumer is gone: whatever it did not pull leaves the process budget. */
|
|
32
|
+
private discard;
|
|
33
|
+
private charge;
|
|
15
34
|
[Symbol.asyncIterator](): AsyncIterator<Buffer>;
|
|
16
35
|
}
|
|
@@ -2,14 +2,31 @@
|
|
|
2
2
|
* Push-driven async iterable shared by the transcription sockets: socket
|
|
3
3
|
* frames are pushed in, `transcribeStream()` pulls them out.
|
|
4
4
|
*
|
|
5
|
+
* The queue reports its level to whoever feeds it, so a socket can be paused
|
|
6
|
+
* when audio arrives faster than the engine takes it and resumed once the
|
|
7
|
+
* backlog drains (TCP backpressure, instead of buffering the whole burst).
|
|
8
|
+
* Every queue also charges its bytes to one process-wide budget, so a burst
|
|
9
|
+
* of uploads across many sockets cannot exhaust the process.
|
|
10
|
+
*
|
|
5
11
|
* @module server/websocket/frameQueue
|
|
6
12
|
*/
|
|
13
|
+
/** Queued bytes across every `FrameQueue` of this process. */
|
|
14
|
+
let processQueuedBytes = 0;
|
|
15
|
+
/** What every queue in the process holds right now, for caps and tests. */
|
|
16
|
+
export function totalQueuedFrameBytes() {
|
|
17
|
+
return processQueuedBytes;
|
|
18
|
+
}
|
|
7
19
|
/** Push-driven async iterable: socket frames in, `transcribeStream()` pulls. */
|
|
8
20
|
export class FrameQueue {
|
|
9
21
|
frames = [];
|
|
10
22
|
waiting;
|
|
11
23
|
ended = false;
|
|
24
|
+
onLevel;
|
|
12
25
|
queuedBytes = 0;
|
|
26
|
+
/** Called after every change of `queuedBytes`, with the new level. */
|
|
27
|
+
setLevelListener(listener) {
|
|
28
|
+
this.onLevel = listener;
|
|
29
|
+
}
|
|
13
30
|
push(frame) {
|
|
14
31
|
if (this.ended) {
|
|
15
32
|
return;
|
|
@@ -21,9 +38,17 @@ export class FrameQueue {
|
|
|
21
38
|
return;
|
|
22
39
|
}
|
|
23
40
|
this.frames.push(frame);
|
|
24
|
-
this.
|
|
41
|
+
this.charge(frame.length);
|
|
25
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* No more frames will arrive; the ones already queued are still handed
|
|
45
|
+
* out, so a recording whose `end` frame followed its audio is transcribed
|
|
46
|
+
* whole. The consumer's `return()` (see below) is what discards.
|
|
47
|
+
*/
|
|
26
48
|
end() {
|
|
49
|
+
if (this.ended) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
27
52
|
this.ended = true;
|
|
28
53
|
if (this.waiting) {
|
|
29
54
|
const resolve = this.waiting;
|
|
@@ -31,12 +56,25 @@ export class FrameQueue {
|
|
|
31
56
|
resolve({ value: undefined, done: true });
|
|
32
57
|
}
|
|
33
58
|
}
|
|
59
|
+
/** The consumer is gone: whatever it did not pull leaves the process budget. */
|
|
60
|
+
discard() {
|
|
61
|
+
this.end();
|
|
62
|
+
if (this.queuedBytes > 0) {
|
|
63
|
+
this.charge(-this.queuedBytes);
|
|
64
|
+
}
|
|
65
|
+
this.frames.length = 0;
|
|
66
|
+
}
|
|
67
|
+
charge(delta) {
|
|
68
|
+
this.queuedBytes += delta;
|
|
69
|
+
processQueuedBytes = Math.max(0, processQueuedBytes + delta);
|
|
70
|
+
this.onLevel?.(this.queuedBytes);
|
|
71
|
+
}
|
|
34
72
|
[Symbol.asyncIterator]() {
|
|
35
73
|
return {
|
|
36
74
|
next: () => {
|
|
37
75
|
const frame = this.frames.shift();
|
|
38
76
|
if (frame) {
|
|
39
|
-
this.
|
|
77
|
+
this.charge(-frame.length);
|
|
40
78
|
return Promise.resolve({ value: frame, done: false });
|
|
41
79
|
}
|
|
42
80
|
if (this.ended) {
|
|
@@ -47,7 +85,7 @@ export class FrameQueue {
|
|
|
47
85
|
});
|
|
48
86
|
},
|
|
49
87
|
return: () => {
|
|
50
|
-
this.
|
|
88
|
+
this.discard();
|
|
51
89
|
return Promise.resolve({ value: undefined, done: true });
|
|
52
90
|
},
|
|
53
91
|
};
|