@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.
Files changed (214) hide show
  1. package/CHANGELOG.md +5 -2
  2. package/README.md +1 -1
  3. package/dist/action/actionExecutor.d.ts +33 -3
  4. package/dist/action/actionExecutor.js +71 -15
  5. package/dist/action/actionInputs.js +4 -1
  6. package/dist/action/cliVersion.d.ts +25 -0
  7. package/dist/action/cliVersion.js +26 -0
  8. package/dist/action/index.d.ts +2 -1
  9. package/dist/action/index.js +2 -1
  10. package/dist/adapters/video/directorPipeline.js +46 -36
  11. package/dist/adapters/video/ffmpegAdapter.d.ts +17 -0
  12. package/dist/adapters/video/ffmpegAdapter.js +32 -5
  13. package/dist/adapters/video/frameExtractor.js +1 -1
  14. package/dist/adapters/video/videoMerger.js +5 -2
  15. package/dist/agent/arithmeticEvaluator.d.ts +32 -0
  16. package/dist/agent/arithmeticEvaluator.js +349 -0
  17. package/dist/agent/directTools.d.ts +6 -18
  18. package/dist/agent/directTools.js +17 -56
  19. package/dist/artifacts/artifactStore.d.ts +22 -1
  20. package/dist/artifacts/artifactStore.js +66 -6
  21. package/dist/auth/anthropicOAuth.d.ts +26 -1
  22. package/dist/auth/anthropicOAuth.js +81 -5
  23. package/dist/auth/secureFiles.d.ts +37 -0
  24. package/dist/auth/secureFiles.js +95 -0
  25. package/dist/auth/tokenStore.d.ts +43 -6
  26. package/dist/auth/tokenStore.js +260 -79
  27. package/dist/avatar/providers/DIDAvatar.js +49 -45
  28. package/dist/avatar/providers/ReplicateAvatar.js +33 -40
  29. package/dist/browser/neurolink.min.js +485 -558
  30. package/dist/cli/commands/auth.js +112 -70
  31. package/dist/cli/commands/proxy.d.ts +4 -0
  32. package/dist/cli/commands/proxy.js +128 -13
  33. package/dist/cli/commands/serve.d.ts +10 -1
  34. package/dist/cli/commands/serve.js +98 -17
  35. package/dist/cli/commands/server.js +17 -5
  36. package/dist/cli/index.js +146 -5
  37. package/dist/cli/loop/optionsSchema.d.ts +1 -1
  38. package/dist/cli/utils/envManager.js +17 -3
  39. package/dist/cli/utils/serverUtils.d.ts +39 -3
  40. package/dist/cli/utils/serverUtils.js +63 -6
  41. package/dist/core/baseProvider.js +7 -1
  42. package/dist/core/modules/MessageBuilder.js +5 -0
  43. package/dist/core/modules/ToolsManager.d.ts +46 -1
  44. package/dist/core/modules/ToolsManager.js +190 -43
  45. package/dist/files/fileReferenceRegistry.d.ts +2 -23
  46. package/dist/files/fileReferenceRegistry.js +55 -5
  47. package/dist/hitl/hitlManager.js +1 -0
  48. package/dist/index.d.ts +2 -1
  49. package/dist/index.js +6 -1
  50. package/dist/mcp/auth/tokenStorage.d.ts +7 -0
  51. package/dist/mcp/auth/tokenStorage.js +23 -4
  52. package/dist/mcp/externalServerManager.d.ts +13 -3
  53. package/dist/mcp/externalServerManager.js +154 -136
  54. package/dist/mcp/index.d.ts +2 -0
  55. package/dist/mcp/index.js +3 -0
  56. package/dist/mcp/mcpClientFactory.d.ts +14 -0
  57. package/dist/mcp/mcpClientFactory.js +70 -7
  58. package/dist/mcp/projectConfigTrust.d.ts +72 -0
  59. package/dist/mcp/projectConfigTrust.js +237 -0
  60. package/dist/mcp/transportUrlPolicy.d.ts +39 -0
  61. package/dist/mcp/transportUrlPolicy.js +172 -0
  62. package/dist/music/providers/BeatovenMusic.js +35 -46
  63. package/dist/music/providers/ReplicateMusic.js +33 -40
  64. package/dist/neurolink.d.ts +16 -1
  65. package/dist/neurolink.js +64 -11
  66. package/dist/processors/archive/ArchiveProcessor.js +15 -1
  67. package/dist/processors/archive/zipEntryIndex.d.ts +22 -0
  68. package/dist/processors/archive/zipEntryIndex.js +33 -0
  69. package/dist/processors/base/BaseFileProcessor.d.ts +9 -21
  70. package/dist/processors/base/BaseFileProcessor.js +40 -111
  71. package/dist/processors/data/YamlProcessor.d.ts +13 -1
  72. package/dist/processors/data/YamlProcessor.js +93 -2
  73. package/dist/processors/document/OpenDocumentProcessor.js +7 -2
  74. package/dist/processors/document/PptxProcessor.js +12 -7
  75. package/dist/processors/media/VideoProcessor.js +93 -15
  76. package/dist/providers/openaiChatCompletionsClient.js +12 -3
  77. package/dist/proxy/accountLedger.js +6 -8
  78. package/dist/proxy/geminiFormat.d.ts +3 -11
  79. package/dist/proxy/geminiFormat.js +11 -2
  80. package/dist/proxy/inboundAuth.d.ts +89 -0
  81. package/dist/proxy/inboundAuth.js +219 -0
  82. package/dist/proxy/openaiFormat.d.ts +3 -9
  83. package/dist/proxy/openaiFormat.js +21 -15
  84. package/dist/proxy/proxyTranslationEngine.d.ts +2 -6
  85. package/dist/proxy/proxyTranslationEngine.js +25 -14
  86. package/dist/proxy/routingPolicy.d.ts +3 -1
  87. package/dist/proxy/routingPolicy.js +7 -8
  88. package/dist/proxy/tokenRefresh.d.ts +6 -0
  89. package/dist/proxy/tokenRefresh.js +13 -1
  90. package/dist/proxy/updateState.js +4 -4
  91. package/dist/rag/document/loaders.d.ts +0 -6
  92. package/dist/rag/document/loaders.js +16 -10
  93. package/dist/rag/ragIntegration.d.ts +2 -2
  94. package/dist/rag/ragIntegration.js +35 -10
  95. package/dist/rag/retrieval/vectorQueryTool.d.ts +7 -0
  96. package/dist/rag/retrieval/vectorQueryTool.js +54 -17
  97. package/dist/server/abstract/baseServerAdapter.d.ts +43 -0
  98. package/dist/server/abstract/baseServerAdapter.js +119 -6
  99. package/dist/server/adapters/expressAdapter.d.ts +3 -0
  100. package/dist/server/adapters/expressAdapter.js +78 -6
  101. package/dist/server/adapters/fastifyAdapter.d.ts +3 -0
  102. package/dist/server/adapters/fastifyAdapter.js +165 -70
  103. package/dist/server/adapters/honoAdapter.d.ts +9 -1
  104. package/dist/server/adapters/honoAdapter.js +99 -21
  105. package/dist/server/adapters/koaAdapter.d.ts +1 -0
  106. package/dist/server/adapters/koaAdapter.js +153 -74
  107. package/dist/server/index.d.ts +2 -0
  108. package/dist/server/index.js +4 -0
  109. package/dist/server/middleware/auth.js +6 -2
  110. package/dist/server/middleware/cache.js +6 -1
  111. package/dist/server/middleware/rateLimit.js +29 -4
  112. package/dist/server/routes/agentRoutes.js +24 -11
  113. package/dist/server/routes/claudeProxyRoutes.d.ts +1 -0
  114. package/dist/server/routes/claudeProxyRoutes.js +273 -84
  115. package/dist/server/routes/codexProxyRoutes.d.ts +2 -2
  116. package/dist/server/routes/codexProxyRoutes.js +125 -40
  117. package/dist/server/routes/index.d.ts +4 -0
  118. package/dist/server/routes/index.js +35 -4
  119. package/dist/server/routes/mcpRoutes.d.ts +6 -2
  120. package/dist/server/routes/mcpRoutes.js +21 -7
  121. package/dist/server/routes/memoryRoutes.js +7 -6
  122. package/dist/server/routes/toolRoutes.d.ts +12 -2
  123. package/dist/server/routes/toolRoutes.js +33 -3
  124. package/dist/server/routes/transcribeRoutes.js +60 -13
  125. package/dist/server/utils/clientAddress.d.ts +23 -0
  126. package/dist/server/utils/clientAddress.js +65 -0
  127. package/dist/server/utils/clientError.d.ts +35 -0
  128. package/dist/server/utils/clientError.js +129 -0
  129. package/dist/server/utils/pathMatch.d.ts +21 -0
  130. package/dist/server/utils/pathMatch.js +41 -0
  131. package/dist/server/utils/validation.d.ts +9 -0
  132. package/dist/server/utils/validation.js +9 -0
  133. package/dist/server/websocket/WebSocketHandler.js +22 -3
  134. package/dist/server/websocket/frameQueue.d.ts +19 -0
  135. package/dist/server/websocket/frameQueue.js +41 -3
  136. package/dist/server/websocket/transcribeWebSocket.d.ts +8 -1
  137. package/dist/server/websocket/transcribeWebSocket.js +223 -34
  138. package/dist/skills/skillStores.js +5 -0
  139. package/dist/tasks/store/fileTaskStore.d.ts +2 -0
  140. package/dist/tasks/store/fileTaskStore.js +11 -4
  141. package/dist/types/attachments.d.ts +117 -0
  142. package/dist/types/attachments.js +15 -0
  143. package/dist/types/auth.d.ts +6 -0
  144. package/dist/types/cli.d.ts +4 -0
  145. package/dist/types/config.d.ts +56 -0
  146. package/dist/types/file.d.ts +8 -0
  147. package/dist/types/hitl.d.ts +8 -0
  148. package/dist/types/index.d.ts +1 -0
  149. package/dist/types/index.js +1 -0
  150. package/dist/types/mcp.d.ts +98 -0
  151. package/dist/types/multimodal.js +0 -41
  152. package/dist/types/openaiCompatible.d.ts +1 -1
  153. package/dist/types/processor.d.ts +14 -1
  154. package/dist/types/proxy.d.ts +71 -16
  155. package/dist/types/proxyClient.d.ts +2 -0
  156. package/dist/types/rag.d.ts +9 -0
  157. package/dist/types/safeFetch.d.ts +40 -0
  158. package/dist/types/server.d.ts +206 -10
  159. package/dist/types/tools.d.ts +57 -0
  160. package/dist/types/transcribe.d.ts +6 -0
  161. package/dist/utils/attachmentPolicy.d.ts +89 -0
  162. package/dist/utils/attachmentPolicy.js +327 -0
  163. package/dist/utils/dotenvBootstrap.d.ts +48 -19
  164. package/dist/utils/dotenvBootstrap.js +172 -31
  165. package/dist/utils/fileDetector.js +115 -65
  166. package/dist/utils/json/coerce.js +11 -1
  167. package/dist/utils/logSanitize.d.ts +5 -0
  168. package/dist/utils/logSanitize.js +8 -0
  169. package/dist/utils/logger.js +73 -1
  170. package/dist/utils/messageBuilder.d.ts +2 -2
  171. package/dist/utils/messageBuilder.js +118 -105
  172. package/dist/utils/multimodalOptionsBuilder.d.ts +1 -0
  173. package/dist/utils/multimodalOptionsBuilder.js +2 -0
  174. package/dist/utils/redis.d.ts +12 -0
  175. package/dist/utils/redis.js +72 -0
  176. package/dist/utils/retryAfter.d.ts +32 -0
  177. package/dist/utils/retryAfter.js +135 -11
  178. package/dist/utils/safeFetch.d.ts +22 -1
  179. package/dist/utils/safeFetch.js +132 -38
  180. package/dist/utils/safeStoreId.d.ts +19 -0
  181. package/dist/utils/safeStoreId.js +29 -0
  182. package/dist/utils/schemaConversion.d.ts +30 -3
  183. package/dist/utils/schemaConversion.js +457 -84
  184. package/dist/utils/sizeGuard.js +2 -2
  185. package/dist/utils/ssrfGuard.d.ts +3 -3
  186. package/dist/utils/ssrfGuard.js +71 -39
  187. package/dist/voice/eval/sttEvalStore.d.ts +26 -3
  188. package/dist/voice/eval/sttEvalStore.js +77 -12
  189. package/dist/voice/livekit/agentDefinition.d.ts +18 -0
  190. package/dist/voice/livekit/agentDefinition.js +22 -0
  191. package/dist/voice/livekit/realtimeMcpTools.d.ts +1 -1
  192. package/dist/voice/livekit/realtimeMcpTools.js +8 -7
  193. package/dist/voice/livekit/realtimeVoiceAgent.d.ts +2 -4
  194. package/dist/voice/livekit/realtimeVoiceAgent.js +18 -7
  195. package/dist/voice/livekit/voiceAgent.d.ts +2 -2
  196. package/dist/voice/livekit/voiceAgent.js +2 -1
  197. package/dist/voice/livekit/xaiSessionWatch.d.ts +0 -5
  198. package/dist/voice/livekit/xaiSessionWatch.js +30 -0
  199. package/dist/voice/providers/GeminiLive.js +21 -6
  200. package/dist/voice/providers/GoogleSTT.js +8 -5
  201. package/dist/voice/transcribe.d.ts +2 -2
  202. package/dist/voice/transcribe.js +34 -18
  203. package/dist/voice/whistle/audio.js +11 -5
  204. package/dist/workflow/workflows/adaptiveWorkflow.d.ts +2 -2
  205. package/dist/workflow/workflows/adaptiveWorkflow.js +10 -10
  206. package/dist/workflow/workflows/consensusWorkflow.d.ts +4 -4
  207. package/dist/workflow/workflows/consensusWorkflow.js +12 -12
  208. package/dist/workflow/workflows/fallbackWorkflow.d.ts +3 -3
  209. package/dist/workflow/workflows/fallbackWorkflow.js +7 -7
  210. package/dist/workflow/workflows/multiJudgeWorkflow.d.ts +7 -7
  211. package/dist/workflow/workflows/multiJudgeWorkflow.js +31 -31
  212. package/docs-site/static/search-index.json +74 -52
  213. package/package.json +173 -156
  214. package/scripts/observability/import-openobserve-dashboard.mjs +11 -3
@@ -725,6 +725,8 @@ export const parseSSEStream = async (body, onTextDelta, onReasoningDelta) => {
725
725
  };
726
726
  const decoder = new TextDecoder();
727
727
  let parseErr;
728
+ // Key of the tool call the previous delta wrote to (for unindexed deltas).
729
+ let lastToolCallKey;
728
730
  const handleEvent = (msg) => {
729
731
  const data = msg.data;
730
732
  if (!data || data === "[DONE]") {
@@ -768,14 +770,21 @@ export const parseSSEStream = async (body, onTextDelta, onReasoningDelta) => {
768
770
  }
769
771
  if (delta?.tool_calls) {
770
772
  for (const tc of delta.tool_calls) {
771
- let state = result.toolCalls.get(tc.index);
773
+ // Gemini's OpenAI-compatible endpoint omits `index`: an unindexed
774
+ // delta with an id or name starts a call, otherwise it continues one.
775
+ const index = tc.index ??
776
+ (tc.id || tc.function?.name || lastToolCallKey === undefined
777
+ ? Math.max(-1, ...result.toolCalls.keys()) + 1
778
+ : lastToolCallKey);
779
+ lastToolCallKey = index;
780
+ let state = result.toolCalls.get(index);
772
781
  if (!state) {
773
782
  state = {
774
- id: tc.id ?? `call_${tc.index}_${Date.now()}`,
783
+ id: tc.id ?? `call_${index}_${Date.now()}`,
775
784
  name: tc.function?.name ?? "",
776
785
  argsBuffered: "",
777
786
  };
778
- result.toolCalls.set(tc.index, state);
787
+ result.toolCalls.set(index, state);
779
788
  }
780
789
  else if (tc.id) {
781
790
  state.id = tc.id;
@@ -28,6 +28,7 @@
28
28
  import { homedir } from "node:os";
29
29
  import { join } from "node:path";
30
30
  import { calculateCost, hasPricing } from "../utils/pricing.js";
31
+ import { proxyTokenUsage } from "./proxyTokenUsage.js";
31
32
  /** Same pattern proxyAnalysis.ts uses, deliberately duplicated as a constant. */
32
33
  const REQUEST_FILE_PATTERN = /^proxy-\d{4}-\d{2}-\d{2}\.jsonl$/;
33
34
  /**
@@ -163,6 +164,7 @@ async function advanceCursor(fileName, cursor) {
163
164
  model: typeof record.model === "string" ? record.model : "",
164
165
  provider: typeof record.provider === "string" ? record.provider : undefined,
165
166
  inputTokens: finiteNumber(record.inputTokens),
167
+ inputIncludesCachedTokens: record.inputIncludesCachedTokens === true,
166
168
  outputTokens: finiteNumber(record.outputTokens),
167
169
  cacheReadTokens: finiteNumber(record.cacheReadTokens),
168
170
  cacheCreationTokens: finiteNumber(record.cacheCreationTokens),
@@ -196,6 +198,9 @@ async function advanceCursor(fileName, cursor) {
196
198
  ...previous,
197
199
  ...next,
198
200
  inputTokens: Math.max(previous.inputTokens, next.inputTokens),
201
+ inputIncludesCachedTokens: next.inputTokens > previous.inputTokens
202
+ ? next.inputIncludesCachedTokens
203
+ : previous.inputIncludesCachedTokens,
199
204
  outputTokens: Math.max(previous.outputTokens, next.outputTokens),
200
205
  cacheReadTokens: Math.max(previous.cacheReadTokens, next.cacheReadTokens),
201
206
  cacheCreationTokens: Math.max(previous.cacheCreationTokens, next.cacheCreationTokens),
@@ -335,14 +340,7 @@ export async function readAccountUsage(date = currentUsageDate()) {
335
340
  const provider = resolveProvider(entry);
336
341
  if (entry.model && entry.model !== "-") {
337
342
  if (hasPricing(provider, entry.model)) {
338
- const cost = calculateCost(provider, entry.model, {
339
- input: entry.inputTokens,
340
- output: entry.outputTokens,
341
- total: entry.inputTokens + entry.outputTokens,
342
- cacheReadTokens: entry.cacheReadTokens,
343
- cacheCreationTokens: entry.cacheCreationTokens,
344
- cacheCreation1hTokens: entry.cacheCreation1hTokens,
345
- });
343
+ const cost = calculateCost(provider, entry.model, proxyTokenUsage(entry));
346
344
  row.costUsd += cost;
347
345
  client.costUsd += cost;
348
346
  }
@@ -18,7 +18,7 @@
18
18
  * its own usage line, so getting them wrong is visible to the user rather than
19
19
  * merely wrong in a log.
20
20
  */
21
- import type { ParsedGeminiRequest, StreamSerializerAdapter } from "../types/index.js";
21
+ import type { ParsedGeminiRequest, StreamSerializerAdapter, TokenUsage } from "../types/index.js";
22
22
  /**
23
23
  * Parse a `generateContent` body into the shape the translation engine takes.
24
24
  *
@@ -46,11 +46,7 @@ export declare function parseGeminiRequest(model: string, body: Record<string, u
46
46
  * cannot drift.
47
47
  */
48
48
  export declare function renderGeminiToolUse(name: string, input: unknown): string;
49
- export declare function buildGeminiResponse(text: string, finishReason: string, usage: {
50
- input: number;
51
- output: number;
52
- total: number;
53
- }, modelVersion: string, toolCalls?: ReadonlyArray<{
49
+ export declare function buildGeminiResponse(text: string, finishReason: string, usage: TokenUsage, modelVersion: string, toolCalls?: ReadonlyArray<{
54
50
  toolName: string;
55
51
  args: Record<string, unknown>;
56
52
  }>): Record<string, unknown>;
@@ -81,11 +77,7 @@ export declare class GeminiStreamSerializer {
81
77
  * that is never coming; rendering it as text keeps the turn terminating.
82
78
  */
83
79
  pushToolUse(_id: string, name: string, input: unknown): string[];
84
- finish(finishReason: string, usage: {
85
- input: number;
86
- output: number;
87
- total: number;
88
- }): string[];
80
+ finish(finishReason: string, usage: TokenUsage): string[];
89
81
  /**
90
82
  * Errors ride the stream as a Google error object.
91
83
  *
@@ -206,10 +206,19 @@ function toGeminiFinishReason(reason) {
206
206
  }
207
207
  }
208
208
  function usageMetadata(usage) {
209
+ const promptTokens = usage.input +
210
+ (usage.cacheReadTokens ?? 0) +
211
+ (usage.cacheCreationTokens ?? 0);
209
212
  return {
210
- promptTokenCount: usage.input,
213
+ promptTokenCount: promptTokens,
211
214
  candidatesTokenCount: usage.output,
212
- totalTokenCount: usage.total || usage.input + usage.output,
215
+ totalTokenCount: usage.total || promptTokens + usage.output,
216
+ ...(usage.cacheReadTokens !== undefined && {
217
+ cachedContentTokenCount: usage.cacheReadTokens,
218
+ }),
219
+ ...(usage.reasoning !== undefined && {
220
+ thoughtsTokenCount: usage.reasoning,
221
+ }),
213
222
  };
214
223
  }
215
224
  /** Build a complete `generateContent` response body. */
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Inbound trust for the proxy listeners and the SDK-mounted proxy routes.
3
+ *
4
+ * Both pool engines spend the operator's subscriptions on behalf of whoever
5
+ * reaches the port. On loopback that is the operator; anywhere else it is
6
+ * anyone. This module holds the three decisions that keep that honest:
7
+ *
8
+ * - **Bind policy.** A listener may bind off-loopback only when something
9
+ * authenticates callers: the share gate, an inbound token, or the operator's
10
+ * explicit `--allow-unauthenticated`.
11
+ * - **Inbound token.** `NEUROLINK_PROXY_INBOUND_TOKEN` (or `--inbound-token`)
12
+ * is a bearer every caller must present, compared in constant time. A share
13
+ * token is an alternative credential, judged by the share gate.
14
+ * - **Operator-only surfaces.** `/accounts`, `/limits` and `/status` name the
15
+ * pool's accounts; they are refused to borrowers and, when a token is
16
+ * configured, to anyone without it.
17
+ *
18
+ * @module proxy/inboundAuth
19
+ */
20
+ import type { ProxyBindPolicyDecision, ProxyBindPolicyInput, ProxyInboundAuthOptions, ProxyShareRefusalResponse } from "../types/index.js";
21
+ /** Dedicated header, so the inbound token never collides with a client credential. */
22
+ export declare const INBOUND_TOKEN_HEADER = "x-neurolink-inbound-token";
23
+ /** Set on every refusal so a client can tell "token required" from "bad request". */
24
+ export declare const INBOUND_AUTH_HEADER = "x-neurolink-inbound-auth";
25
+ export declare const INBOUND_TOKEN_ENV = "NEUROLINK_PROXY_INBOUND_TOKEN";
26
+ export declare const ALLOW_UNAUTHENTICATED_ENV = "NEUROLINK_PROXY_ALLOW_UNAUTHENTICATED";
27
+ /**
28
+ * Compare two secrets without leaking where they diverge. Hand-rolled rather
29
+ * than `crypto.timingSafeEqual` so the proxy routes stay loadable from a build
30
+ * whose `node:crypto` stub does not carry it.
31
+ */
32
+ export declare function secretsMatch(expected: string, presented: string): boolean;
33
+ /** The inbound token in force: an explicit value wins over the environment. */
34
+ export declare function resolveInboundToken(explicit?: string): string | undefined;
35
+ /** `NEUROLINK_PROXY_ALLOW_UNAUTHENTICATED` as a boolean. */
36
+ export declare function isUnauthenticatedBindAllowedByEnv(): boolean;
37
+ /** Loopback addresses in every spelling a bind flag accepts. */
38
+ export declare function isLoopbackHost(host: string): boolean;
39
+ /**
40
+ * May the proxy bind to `host`?
41
+ *
42
+ * Loopback always may. Anything else needs the share gate, an inbound token,
43
+ * or the operator's explicit acknowledgement — and the acknowledgement still
44
+ * earns a warning, because the subscriptions are then one port scan away.
45
+ */
46
+ export declare function evaluateProxyBindPolicy(input: ProxyBindPolicyInput): ProxyBindPolicyDecision;
47
+ /** True when the request presents the configured inbound token. */
48
+ export declare function hasValidInboundToken(headers: Record<string, string | undefined>, token: string | undefined): boolean;
49
+ /** The 401 an unauthenticated caller receives, in the Anthropic error shape. */
50
+ export declare function buildInboundAuthRefusal(): ProxyShareRefusalResponse;
51
+ /**
52
+ * Decide an inbound request against the configured token.
53
+ *
54
+ * A request carrying a share token is handed to the share gate instead: a
55
+ * borrower holds a grant, not the operator's token, and the gate is what
56
+ * judges a grant. Everything else must present the token.
57
+ */
58
+ export declare function authorizeInboundRequest(headers: Record<string, string | undefined>, token: string | undefined): {
59
+ ok: true;
60
+ } | {
61
+ ok: false;
62
+ response: ProxyShareRefusalResponse;
63
+ };
64
+ /** `Response` form of the refusal, for route factories whose adapters pass one through. */
65
+ export declare function inboundAuthRefusalResponse(): Response;
66
+ /**
67
+ * Say, loudly and once per engine, that a proxy route group was mounted with
68
+ * nothing authenticating its callers. `logger.always` on purpose: the ordinary
69
+ * warn level is invisible outside debug runs, and an open proxy is exactly the
70
+ * thing an operator must not be able to miss. Suppressed when the caller
71
+ * recorded the decision (`allowUnauthenticated`), which the CLI does after its
72
+ * own bind check.
73
+ */
74
+ export declare function warnIfProxyRoutesUnauthenticated(engine: string, options: ProxyInboundAuthOptions | undefined, token: string | undefined): void;
75
+ /** Test isolation only. */
76
+ export declare function resetInboundAuthWarningsForTests(): void;
77
+ /** Paths that stay open on an authenticated listener: liveness and self-authenticating peers. */
78
+ export declare function isInboundAuthExemptPath(path: string): boolean;
79
+ /**
80
+ * How many pooled accounts one inbound request may be tried against.
81
+ *
82
+ * Without a cap a single request fans out to every account in the pool — N
83
+ * upstream posts, N× quota — on any rotation trigger. Default 3;
84
+ * `NEUROLINK_PROXY_MAX_ACCOUNTS_PER_REQUEST=0` restores the unbounded
85
+ * behaviour.
86
+ */
87
+ export declare const MAX_ACCOUNTS_PER_REQUEST_ENV = "NEUROLINK_PROXY_MAX_ACCOUNTS_PER_REQUEST";
88
+ export declare const DEFAULT_MAX_ACCOUNTS_PER_REQUEST = 3;
89
+ export declare function resolveMaxAccountsPerRequest(): number;
@@ -0,0 +1,219 @@
1
+ /**
2
+ * Inbound trust for the proxy listeners and the SDK-mounted proxy routes.
3
+ *
4
+ * Both pool engines spend the operator's subscriptions on behalf of whoever
5
+ * reaches the port. On loopback that is the operator; anywhere else it is
6
+ * anyone. This module holds the three decisions that keep that honest:
7
+ *
8
+ * - **Bind policy.** A listener may bind off-loopback only when something
9
+ * authenticates callers: the share gate, an inbound token, or the operator's
10
+ * explicit `--allow-unauthenticated`.
11
+ * - **Inbound token.** `NEUROLINK_PROXY_INBOUND_TOKEN` (or `--inbound-token`)
12
+ * is a bearer every caller must present, compared in constant time. A share
13
+ * token is an alternative credential, judged by the share gate.
14
+ * - **Operator-only surfaces.** `/accounts`, `/limits` and `/status` name the
15
+ * pool's accounts; they are refused to borrowers and, when a token is
16
+ * configured, to anyone without it.
17
+ *
18
+ * @module proxy/inboundAuth
19
+ */
20
+ import { logger } from "../utils/logger.js";
21
+ import { extractShareToken } from "./shareGate.js";
22
+ /** Dedicated header, so the inbound token never collides with a client credential. */
23
+ export const INBOUND_TOKEN_HEADER = "x-neurolink-inbound-token";
24
+ /** Set on every refusal so a client can tell "token required" from "bad request". */
25
+ export const INBOUND_AUTH_HEADER = "x-neurolink-inbound-auth";
26
+ export const INBOUND_TOKEN_ENV = "NEUROLINK_PROXY_INBOUND_TOKEN";
27
+ export const ALLOW_UNAUTHENTICATED_ENV = "NEUROLINK_PROXY_ALLOW_UNAUTHENTICATED";
28
+ /**
29
+ * Compare two secrets without leaking where they diverge. Hand-rolled rather
30
+ * than `crypto.timingSafeEqual` so the proxy routes stay loadable from a build
31
+ * whose `node:crypto` stub does not carry it.
32
+ */
33
+ export function secretsMatch(expected, presented) {
34
+ if (expected.length !== presented.length || expected.length === 0) {
35
+ return false;
36
+ }
37
+ let difference = 0;
38
+ for (let index = 0; index < expected.length; index += 1) {
39
+ difference |= expected.charCodeAt(index) ^ presented.charCodeAt(index);
40
+ }
41
+ return difference === 0;
42
+ }
43
+ function envFlagIsOn(name) {
44
+ const raw = (process.env[name] ?? "").trim().toLowerCase();
45
+ return raw === "1" || raw === "true" || raw === "on" || raw === "yes";
46
+ }
47
+ /** The inbound token in force: an explicit value wins over the environment. */
48
+ export function resolveInboundToken(explicit) {
49
+ const candidate = explicit?.trim() || process.env[INBOUND_TOKEN_ENV]?.trim();
50
+ return candidate ? candidate : undefined;
51
+ }
52
+ /** `NEUROLINK_PROXY_ALLOW_UNAUTHENTICATED` as a boolean. */
53
+ export function isUnauthenticatedBindAllowedByEnv() {
54
+ return envFlagIsOn(ALLOW_UNAUTHENTICATED_ENV);
55
+ }
56
+ /** Loopback addresses in every spelling a bind flag accepts. */
57
+ export function isLoopbackHost(host) {
58
+ const trimmed = host
59
+ .trim()
60
+ .toLowerCase()
61
+ .replace(/^\[|\]$/g, "");
62
+ if (trimmed === "localhost" || trimmed === "::1") {
63
+ return true;
64
+ }
65
+ if (/^0{1,4}(:0{1,4}){6}:0{0,3}1$/.test(trimmed)) {
66
+ return true;
67
+ }
68
+ if (trimmed.startsWith("::ffff:")) {
69
+ return isLoopbackHost(trimmed.slice("::ffff:".length));
70
+ }
71
+ const octets = trimmed.split(".");
72
+ return (octets.length === 4 &&
73
+ octets[0] === "127" &&
74
+ octets.every((octet) => /^\d{1,3}$/.test(octet) && Number(octet) <= 255));
75
+ }
76
+ /**
77
+ * May the proxy bind to `host`?
78
+ *
79
+ * Loopback always may. Anything else needs the share gate, an inbound token,
80
+ * or the operator's explicit acknowledgement — and the acknowledgement still
81
+ * earns a warning, because the subscriptions are then one port scan away.
82
+ */
83
+ export function evaluateProxyBindPolicy(input) {
84
+ if (isLoopbackHost(input.host)) {
85
+ return { allowed: true };
86
+ }
87
+ if (input.inboundToken || input.requireGrant) {
88
+ return { allowed: true };
89
+ }
90
+ if (input.allowUnauthenticated) {
91
+ return {
92
+ allowed: true,
93
+ warning: `Binding ${input.host} with no inbound authentication: anyone who reaches ` +
94
+ `this port can spend every pooled subscription. --allow-unauthenticated ` +
95
+ `acknowledges that; put an authenticating reverse proxy in front.`,
96
+ };
97
+ }
98
+ return {
99
+ allowed: false,
100
+ reason: `Refusing to bind ${input.host}: it is not a loopback address and nothing ` +
101
+ `would authenticate inbound callers. Set ${INBOUND_TOKEN_ENV} (or pass ` +
102
+ `--inbound-token <secret>), gate the port with NEUROLINK_PROXY_REQUIRE_GRANT=1, ` +
103
+ `or pass --allow-unauthenticated to accept an open proxy.`,
104
+ };
105
+ }
106
+ function presentedInboundToken(headers) {
107
+ const dedicated = headers[INBOUND_TOKEN_HEADER];
108
+ if (dedicated?.trim()) {
109
+ return dedicated.trim();
110
+ }
111
+ const authorization = headers.authorization ?? headers.Authorization;
112
+ if (!authorization) {
113
+ return undefined;
114
+ }
115
+ const trimmed = authorization.trim();
116
+ return trimmed.toLowerCase().startsWith("bearer ")
117
+ ? trimmed.slice("bearer ".length).trim()
118
+ : undefined;
119
+ }
120
+ /** True when the request presents the configured inbound token. */
121
+ export function hasValidInboundToken(headers, token) {
122
+ if (!token) {
123
+ return false;
124
+ }
125
+ const presented = presentedInboundToken(headers);
126
+ return presented !== undefined && secretsMatch(token, presented);
127
+ }
128
+ /** The 401 an unauthenticated caller receives, in the Anthropic error shape. */
129
+ export function buildInboundAuthRefusal() {
130
+ return {
131
+ status: 401,
132
+ headers: { [INBOUND_AUTH_HEADER]: "required" },
133
+ body: {
134
+ type: "error",
135
+ error: {
136
+ type: "authentication_error",
137
+ message: "This proxy requires an inbound token. Send it as " +
138
+ `${INBOUND_TOKEN_HEADER} or as Authorization: Bearer <token>.`,
139
+ },
140
+ },
141
+ };
142
+ }
143
+ /**
144
+ * Decide an inbound request against the configured token.
145
+ *
146
+ * A request carrying a share token is handed to the share gate instead: a
147
+ * borrower holds a grant, not the operator's token, and the gate is what
148
+ * judges a grant. Everything else must present the token.
149
+ */
150
+ export function authorizeInboundRequest(headers, token) {
151
+ if (!token) {
152
+ return { ok: true };
153
+ }
154
+ if (hasValidInboundToken(headers, token)) {
155
+ return { ok: true };
156
+ }
157
+ if (extractShareToken(headers) !== undefined) {
158
+ return { ok: true };
159
+ }
160
+ return { ok: false, response: buildInboundAuthRefusal() };
161
+ }
162
+ /** `Response` form of the refusal, for route factories whose adapters pass one through. */
163
+ export function inboundAuthRefusalResponse() {
164
+ const refusal = buildInboundAuthRefusal();
165
+ return new Response(JSON.stringify(refusal.body), {
166
+ status: refusal.status,
167
+ headers: { "content-type": "application/json", ...refusal.headers },
168
+ });
169
+ }
170
+ const warnedEngines = new Set();
171
+ /**
172
+ * Say, loudly and once per engine, that a proxy route group was mounted with
173
+ * nothing authenticating its callers. `logger.always` on purpose: the ordinary
174
+ * warn level is invisible outside debug runs, and an open proxy is exactly the
175
+ * thing an operator must not be able to miss. Suppressed when the caller
176
+ * recorded the decision (`allowUnauthenticated`), which the CLI does after its
177
+ * own bind check.
178
+ */
179
+ export function warnIfProxyRoutesUnauthenticated(engine, options, token) {
180
+ if (token || options?.allowUnauthenticated || warnedEngines.has(engine)) {
181
+ return;
182
+ }
183
+ warnedEngines.add(engine);
184
+ logger.always(`[proxy] SECURITY: the ${engine} proxy routes were mounted with no inbound ` +
185
+ `authentication. Anyone who can reach them spends the pooled subscriptions. ` +
186
+ `Set ${INBOUND_TOKEN_ENV}, pass inboundAuth.token, or bind loopback only ` +
187
+ `and set inboundAuth.allowUnauthenticated to acknowledge this.`);
188
+ }
189
+ /** Test isolation only. */
190
+ export function resetInboundAuthWarningsForTests() {
191
+ warnedEngines.clear();
192
+ }
193
+ /** Paths that stay open on an authenticated listener: liveness and self-authenticating peers. */
194
+ export function isInboundAuthExemptPath(path) {
195
+ return (path === "/health" ||
196
+ path === "/internal/update-control" ||
197
+ path.startsWith("/peer/"));
198
+ }
199
+ /**
200
+ * How many pooled accounts one inbound request may be tried against.
201
+ *
202
+ * Without a cap a single request fans out to every account in the pool — N
203
+ * upstream posts, N× quota — on any rotation trigger. Default 3;
204
+ * `NEUROLINK_PROXY_MAX_ACCOUNTS_PER_REQUEST=0` restores the unbounded
205
+ * behaviour.
206
+ */
207
+ export const MAX_ACCOUNTS_PER_REQUEST_ENV = "NEUROLINK_PROXY_MAX_ACCOUNTS_PER_REQUEST";
208
+ export const DEFAULT_MAX_ACCOUNTS_PER_REQUEST = 3;
209
+ export function resolveMaxAccountsPerRequest() {
210
+ const raw = process.env[MAX_ACCOUNTS_PER_REQUEST_ENV]?.trim();
211
+ if (raw === undefined || raw === "") {
212
+ return DEFAULT_MAX_ACCOUNTS_PER_REQUEST;
213
+ }
214
+ const parsed = Number(raw);
215
+ if (!Number.isFinite(parsed) || parsed < 0) {
216
+ return DEFAULT_MAX_ACCOUNTS_PER_REQUEST;
217
+ }
218
+ return parsed === 0 ? Number.POSITIVE_INFINITY : Math.floor(parsed);
219
+ }
@@ -8,7 +8,7 @@
8
8
  *
9
9
  * Reference: https://platform.openai.com/docs/api-reference/chat/create
10
10
  */
11
- import type { ClaudeRequest, ClaudeResponse, InternalResult, OpenAICompletionRequest, OpenAICompletionResponse, OpenAIErrorResponse, ParsedOpenAIRequest, StreamLifecycleState } from "../types/index.js";
11
+ import type { ClaudeRequest, ClaudeResponse, InternalResult, OpenAICompletionRequest, OpenAICompletionResponse, OpenAIErrorResponse, ParsedOpenAIRequest, StreamLifecycleState, TokenUsage } from "../types/index.js";
12
12
  /** Generate a unique chat completion ID in the OpenAI format. */
13
13
  export declare function generateChatCompletionId(): string;
14
14
  /** Generate an OpenAI-format tool call ID (`call_` + random chars). */
@@ -25,9 +25,7 @@ export declare function generateOpenAIToolCallId(): string;
25
25
  * - top_p, temperature, max_tokens pass-through
26
26
  */
27
27
  export declare function parseOpenAIRequest(body: OpenAICompletionRequest): ParsedOpenAIRequest;
28
- /**
29
- * Serialize a NeuroLink GenerateResult into an OpenAI Chat Completions response.
30
- */
28
+ /** Serialize a NeuroLink GenerateResult into an OpenAI Chat Completions response. */
31
29
  export declare function serializeOpenAIResponse(result: InternalResult, requestModel: string): OpenAICompletionResponse;
32
30
  /**
33
31
  * Build an OpenAI-compatible error envelope.
@@ -97,11 +95,7 @@ export declare class OpenAIStreamSerializer {
97
95
  /**
98
96
  * Finalize the stream: emit finish_reason chunk, then `data: [DONE]`.
99
97
  */
100
- finish(finishReason?: string, usage?: {
101
- input: number;
102
- output: number;
103
- total: number;
104
- }): Generator<string>;
98
+ finish(finishReason?: string, usage?: TokenUsage): Generator<string>;
105
99
  /**
106
100
  * Emit an error event. Transitions to terminal ERROR state.
107
101
  */
@@ -205,9 +205,25 @@ function mapFinishReason(finishReason) {
205
205
  return finishReason ? "stop" : null;
206
206
  }
207
207
  }
208
- /**
209
- * Serialize a NeuroLink GenerateResult into an OpenAI Chat Completions response.
210
- */
208
+ /** Add cache to the prompt total; reasoning is already part of completion. */
209
+ function toOpenAIUsage(usage) {
210
+ const promptTokens = (usage?.input ?? 0) +
211
+ (usage?.cacheReadTokens ?? 0) +
212
+ (usage?.cacheCreationTokens ?? 0);
213
+ const completionTokens = usage?.output ?? 0;
214
+ return {
215
+ prompt_tokens: promptTokens,
216
+ completion_tokens: completionTokens,
217
+ total_tokens: usage?.total ?? promptTokens + completionTokens,
218
+ ...(usage?.cacheReadTokens !== undefined && {
219
+ prompt_tokens_details: { cached_tokens: usage.cacheReadTokens },
220
+ }),
221
+ ...(usage?.reasoning !== undefined && {
222
+ completion_tokens_details: { reasoning_tokens: usage.reasoning },
223
+ }),
224
+ };
225
+ }
226
+ /** Serialize a NeuroLink GenerateResult into an OpenAI Chat Completions response. */
211
227
  export function serializeOpenAIResponse(result, requestModel) {
212
228
  const inferredFinishReason = result.toolCalls &&
213
229
  result.toolCalls.length > 0 &&
@@ -244,11 +260,7 @@ export function serializeOpenAIResponse(result, requestModel) {
244
260
  finish_reason: mapFinishReason(inferredFinishReason),
245
261
  },
246
262
  ],
247
- usage: {
248
- prompt_tokens: result.usage?.input ?? 0,
249
- completion_tokens: result.usage?.output ?? 0,
250
- total_tokens: result.usage?.total ?? 0,
251
- },
263
+ usage: toOpenAIUsage(result.usage),
252
264
  };
253
265
  }
254
266
  // ---------------------------------------------------------------------------
@@ -450,13 +462,7 @@ export class OpenAIStreamSerializer {
450
462
  return;
451
463
  }
452
464
  const mappedReason = mapFinishReason(finishReason) ?? "stop";
453
- const usagePayload = usage
454
- ? {
455
- prompt_tokens: usage.input,
456
- completion_tokens: usage.output,
457
- total_tokens: usage.total,
458
- }
459
- : undefined;
465
+ const usagePayload = usage ? toOpenAIUsage(usage) : undefined;
460
466
  yield formatOpenAISSE(this.makeChunk({}, mappedReason, usagePayload));
461
467
  yield "data: [DONE]\n\n";
462
468
  this.state = "done";
@@ -12,7 +12,7 @@
12
12
  * JSON handlers that accept a format discriminator.
13
13
  */
14
14
  import type { ProxyTracer } from "./proxyTracer.js";
15
- import type { ParsedClaudeRequest, ParsedGeminiRequest, ParsedOpenAIRequest, ProxyFormat, ProxyTranslationAttempt, ServerContext } from "../types/index.js";
15
+ import type { ParsedClaudeRequest, ParsedGeminiRequest, ParsedOpenAIRequest, ProxyFormat, ProxyTranslationAttempt, ServerContext, TokenUsage } from "../types/index.js";
16
16
  /**
17
17
  * Extract text content from a stream chunk (handles various chunk formats).
18
18
  */
@@ -29,11 +29,7 @@ export declare function hasTranslatedOutput(collectedText: string, toolCalls: un
29
29
  * - AI SDK v4: promptTokens / completionTokens
30
30
  * - NeuroLink internal: input / output
31
31
  */
32
- export declare function extractUsageFromStreamResult(usage: unknown): {
33
- input: number;
34
- output: number;
35
- total: number;
36
- };
32
+ export declare function extractUsageFromStreamResult(usage: unknown): TokenUsage;
37
33
  /**
38
34
  * Detect which proxy format a request is using based on path and headers.
39
35
  */
@@ -77,17 +77,25 @@ export function hasTranslatedOutput(collectedText, toolCalls) {
77
77
  * - NeuroLink internal: input / output
78
78
  */
79
79
  export function extractUsageFromStreamResult(usage) {
80
- if (!usage || typeof usage !== "object") {
81
- return { input: 0, output: 0, total: 0 };
82
- }
83
- const u = usage;
84
- const input = (typeof u.inputTokens === "number" ? u.inputTokens : 0) ||
85
- (typeof u.promptTokens === "number" ? u.promptTokens : 0) ||
86
- (typeof u.input === "number" ? u.input : 0);
87
- const output = (typeof u.outputTokens === "number" ? u.outputTokens : 0) ||
88
- (typeof u.completionTokens === "number" ? u.completionTokens : 0) ||
89
- (typeof u.output === "number" ? u.output : 0);
90
- return { input, output, total: input + output };
80
+ const observed = readTranslationUsage(usage);
81
+ const cacheReadTokens = observed.cacheReadTokens ?? 0;
82
+ const cacheCreationTokens = observed.cacheCreationTokens ?? 0;
83
+ const input = observed.inputIncludesCachedTokens
84
+ ? Math.max(0, (observed.inputTokens ?? 0) - cacheReadTokens - cacheCreationTokens)
85
+ : (observed.inputTokens ?? 0);
86
+ const output = observed.outputTokens ?? 0;
87
+ return {
88
+ input,
89
+ output,
90
+ total: input + cacheReadTokens + cacheCreationTokens + output,
91
+ ...(observed.cacheReadTokens !== undefined ? { cacheReadTokens } : {}),
92
+ ...(observed.cacheCreationTokens !== undefined
93
+ ? { cacheCreationTokens }
94
+ : {}),
95
+ ...(observed.reasoningTokens !== undefined
96
+ ? { reasoning: observed.reasoningTokens }
97
+ : {}),
98
+ };
91
99
  }
92
100
  // ---------------------------------------------------------------------------
93
101
  // Format detection
@@ -250,11 +258,11 @@ function defaultFinishReason(format) {
250
258
  return format === "claude" ? "end_turn" : "stop";
251
259
  }
252
260
  /** Preserve unknown usage as absent; zero is only reported when observed. */
253
- function translationUsage(result) {
254
- const raw = result?.usage;
255
- if (!raw) {
261
+ function readTranslationUsage(usage) {
262
+ if (!usage || typeof usage !== "object") {
256
263
  return {};
257
264
  }
265
+ const raw = usage;
258
266
  const number = (...values) => values.find((value) => typeof value === "number" && Number.isFinite(value) && value >= 0);
259
267
  const details = raw.outputTokenDetails;
260
268
  const inputs = raw.inputTokenDetails;
@@ -275,6 +283,9 @@ function translationUsage(result) {
275
283
  reasoningTokens: number(raw.reasoningTokens, raw.reasoning, details?.reasoningTokens),
276
284
  };
277
285
  }
286
+ function translationUsage(result) {
287
+ return readTranslationUsage(result?.usage);
288
+ }
278
289
  /** Shared request owner, independent of a consumer asking for another chunk. */
279
290
  function createTranslationRequest(args) {
280
291
  const { ctx, requestModel, parsed, tracer, requestStartTime, stream } = args;
@@ -14,6 +14,8 @@ export declare function buildProxyTranslationPlan(primary: {
14
14
  }, fallbackChain: FallbackEntry[], requestedModel: string, _parsed?: ParsedClaudeRequest, allowAutoFallback?: boolean): ProxyTranslationPlan;
15
15
  /**
16
16
  * Parse the retry-after header from an upstream 429 response.
17
- * Returns milliseconds to wait, or 0 if no valid header present.
17
+ *
18
+ * @param retryAfterHeader - The raw Retry-After header string or null
19
+ * @returns Milliseconds to wait (minimum 1000ms if valid), or 0 if no valid header present
18
20
  */
19
21
  export declare function parseRetryAfterMs(retryAfterHeader: string | null): number;
@@ -1,3 +1,4 @@
1
+ import { parseRetryAfterHeaderValue } from "../utils/retryAfter.js";
1
2
  export function inferClaudeProxyModelTier(modelName) {
2
3
  const normalized = modelName.toLowerCase();
3
4
  if (normalized.includes("opus")) {
@@ -59,19 +60,17 @@ export function buildProxyTranslationPlan(primary, fallbackChain, requestedModel
59
60
  // ---------------------------------------------------------------------------
60
61
  /**
61
62
  * Parse the retry-after header from an upstream 429 response.
62
- * Returns milliseconds to wait, or 0 if no valid header present.
63
+ *
64
+ * @param retryAfterHeader - The raw Retry-After header string or null
65
+ * @returns Milliseconds to wait (minimum 1000ms if valid), or 0 if no valid header present
63
66
  */
64
67
  export function parseRetryAfterMs(retryAfterHeader) {
65
68
  if (!retryAfterHeader) {
66
69
  return 0;
67
70
  }
68
- const seconds = parseInt(retryAfterHeader, 10);
69
- if (!Number.isNaN(seconds)) {
70
- return Math.max(1, seconds) * 1000;
71
- }
72
- const date = new Date(retryAfterHeader);
73
- if (!Number.isNaN(date.getTime())) {
74
- return Math.max(1000, date.getTime() - Date.now());
71
+ const delay = parseRetryAfterHeaderValue(retryAfterHeader);
72
+ if (delay !== undefined) {
73
+ return Math.max(1000, delay);
75
74
  }
76
75
  return 0;
77
76
  }