@juspay/neurolink 9.92.2 → 9.93.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.
@@ -10,6 +10,7 @@
10
10
  * (generate/stream), with an optional ModelRouter for model remapping.
11
11
  */
12
12
  import type { CommandModule } from "yargs";
13
+ import type { Hono } from "hono";
13
14
  import type { AccountAllowlist, LoadedProxyConfig, ModelRouterInterface, ProxyGuardArgs, ProxyNeurolinkRuntime, ProxyStartArgs, ProxyStartStrategy, ProxyStatusArgs, ProxyTelemetryArgs } from "../../lib/types/index.js";
14
15
  import { ProxyRuntimeConfigStore } from "../../lib/proxy/runtimeConfig.js";
15
16
  export declare function mapClaudeErrorTypeToStatus(errorType?: string): number;
@@ -25,7 +26,7 @@ export declare function createProxyStartApp(params: {
25
26
  accountAllowlist: AccountAllowlist | undefined;
26
27
  runtimeConfigStore?: ProxyRuntimeConfigStore;
27
28
  }): Promise<{
28
- app: import("hono").Hono<import("hono/types").BlankEnv, import("hono/types").BlankSchema, "/">;
29
+ app: Hono<import("hono/types").BlankEnv, import("hono/types").BlankSchema, "/">;
29
30
  readiness: import("../../lib/types/proxy.js").ProxyReadinessState;
30
31
  }>;
31
32
  export declare const proxyStartCommand: CommandModule<object, ProxyStartArgs>;
@@ -16,11 +16,13 @@ import chalk from "chalk";
16
16
  import ora from "ora";
17
17
  import { buildProxyHealthResponse, createProxyReadinessState, markProxyReady, waitForProxyReadiness, } from "../../lib/proxy/proxyHealth.js";
18
18
  import { logger } from "../../lib/utils/logger.js";
19
+ import { withTimeout } from "../../lib/utils/async/withTimeout.js";
19
20
  import { formatUptime, isProcessRunning, StateFileManager, } from "../utils/serverUtils.js";
20
21
  import { configureProxyKeepAliveDispatcher } from "../../lib/proxy/proxyDispatcher.js";
21
22
  import { ProxyRuntimeConfigStore } from "../../lib/proxy/runtimeConfig.js";
22
23
  import { anthropicAccountKeysEqual, createAccountAllowlist, ENV_ANTHROPIC_ACCOUNT_KEY, isAccountAllowed, LEGACY_ANTHROPIC_ACCOUNT_KEY, normalizeAnthropicAccountKey, shouldLoadFallbackCredential, } from "../../lib/proxy/accountSelection.js";
23
24
  import { beginProxyRequest, getProxyActivitySnapshot, trackProxyResponse, } from "../../lib/proxy/proxyActivity.js";
25
+ import { flushProxyLifecycleEvents, getProxyLifecycleLoggerSnapshot, hashProxyLifecycleSessionId, logProxyLifecycleEvent, } from "../../lib/proxy/proxyLifecycle.js";
24
26
  import { describeInstallFailure, getGlobalInstallArgs, resolveGlobalInstaller, validateInstalledVersion, } from "../../lib/proxy/globalInstaller.js";
25
27
  import { startUpdaterWorkerSupervisor } from "../../lib/proxy/updaterSupervisor.js";
26
28
  import { abandonPendingUpdate, isVersionSuppressed, loadUpdateState, recordCheck, recordSuccessfulUpdate, recordUpdateFailure, recordUpdateInstalled, suppressVersion, } from "../../lib/proxy/updateState.js";
@@ -31,6 +33,7 @@ import packageJson from "../../../package.json" with { type: "json" };
31
33
  const _require = createRequire(import.meta.url);
32
34
  const PROXY_VERSION = packageJson.version;
33
35
  const PROXY_TELEMETRY_SCRIPT_PATH = fileURLToPath(new URL("../../../scripts/observability/manage-local-openobserve.sh", import.meta.url));
36
+ const PROXY_LIFECYCLE_SHUTDOWN_TIMEOUT_MS = 5_000;
34
37
  // =============================================================================
35
38
  // STATE MANAGEMENT
36
39
  // =============================================================================
@@ -643,6 +646,20 @@ export function mapClaudeErrorTypeToStatus(errorType) {
643
646
  return 502;
644
647
  }
645
648
  }
649
+ function getProxyRuntimeErrorCode(error) {
650
+ let current = error;
651
+ for (let depth = 0; depth < 3; depth += 1) {
652
+ if (!current || typeof current !== "object") {
653
+ return undefined;
654
+ }
655
+ const code = current.code;
656
+ if (typeof code === "string") {
657
+ return code;
658
+ }
659
+ current = current.cause;
660
+ }
661
+ return undefined;
662
+ }
646
663
  async function ensureProxyStartAllowed(spinner) {
647
664
  const ignoreLaunchd = process.env.NEUROLINK_PROXY_IGNORE_LAUNCHD === "1" ||
648
665
  process.env.NEUROLINK_PROXY_IGNORE_LAUNCHD === "true";
@@ -710,6 +727,125 @@ async function createProxyNeurolinkRuntime(logsDir) {
710
727
  cleanupLogs(7, 500);
711
728
  return { neurolink, cleanupLogs };
712
729
  }
730
+ function registerProxyRequestTracking(app, requestMetadata) {
731
+ app.use("/v1/*", async (c, next) => {
732
+ const startedMonotonicMs = performance.now();
733
+ const contentLengthHeader = c.req.raw.headers.get("content-length");
734
+ const rawContentLength = contentLengthHeader === null ? Number.NaN : Number(contentLengthHeader);
735
+ const requestBytes = Number.isFinite(rawContentLength) && rawContentLength >= 0
736
+ ? rawContentLength
737
+ : undefined;
738
+ const sessionId = c.req.raw.headers.get("x-neurolink-session-id") ??
739
+ c.req.raw.headers.get("x-claude-code-session-id") ??
740
+ undefined;
741
+ const sessionHash = hashProxyLifecycleSessionId(sessionId);
742
+ const metadata = {
743
+ requestId: crypto.randomUUID(),
744
+ method: c.req.method,
745
+ path: c.req.path,
746
+ startedAt: Date.now(),
747
+ model: "-",
748
+ stream: false,
749
+ toolCount: 0,
750
+ };
751
+ requestMetadata.set(c.req.raw, metadata);
752
+ const finishActivity = beginProxyRequest();
753
+ const finish = () => {
754
+ finishActivity();
755
+ requestMetadata.delete(c.req.raw);
756
+ };
757
+ // The route adapter populates model/stream/toolCount after parsing. Omit
758
+ // them at acceptance instead of publishing misleading placeholder values;
759
+ // subsequent events carry the parsed metadata under the same request ID.
760
+ logProxyLifecycleEvent({
761
+ event: "request_accepted",
762
+ requestId: metadata.requestId,
763
+ method: metadata.method,
764
+ path: metadata.path,
765
+ sessionHash,
766
+ requestBytes,
767
+ elapsedMs: 0,
768
+ monotonicMs: startedMonotonicMs,
769
+ });
770
+ try {
771
+ await next();
772
+ const responseStatus = c.res.status;
773
+ logProxyLifecycleEvent({
774
+ event: "response_headers",
775
+ requestId: metadata.requestId,
776
+ method: metadata.method,
777
+ path: metadata.path,
778
+ model: metadata.model,
779
+ stream: metadata.stream,
780
+ toolCount: metadata.toolCount,
781
+ sessionHash,
782
+ requestBytes,
783
+ responseStatus,
784
+ elapsedMs: performance.now() - startedMonotonicMs,
785
+ });
786
+ c.res = trackProxyResponse(c.res, finish, {
787
+ onFirstChunk: ({ observedBodyBytes, responseChunks }) => {
788
+ logProxyLifecycleEvent({
789
+ event: "response_first_chunk",
790
+ requestId: metadata.requestId,
791
+ method: metadata.method,
792
+ path: metadata.path,
793
+ model: metadata.model,
794
+ stream: metadata.stream,
795
+ toolCount: metadata.toolCount,
796
+ sessionHash,
797
+ requestBytes,
798
+ responseStatus,
799
+ observedBodyBytes,
800
+ responseChunks,
801
+ elapsedMs: performance.now() - startedMonotonicMs,
802
+ });
803
+ },
804
+ onTerminal: ({ outcome, observedBodyBytes, responseChunks }) => {
805
+ logProxyLifecycleEvent({
806
+ event: "request_terminal",
807
+ requestId: metadata.requestId,
808
+ method: metadata.method,
809
+ path: metadata.path,
810
+ model: metadata.model,
811
+ stream: metadata.stream,
812
+ toolCount: metadata.toolCount,
813
+ sessionHash,
814
+ requestBytes,
815
+ responseStatus,
816
+ observedBodyBytes,
817
+ responseChunks,
818
+ elapsedMs: performance.now() - startedMonotonicMs,
819
+ terminalOutcome: outcome,
820
+ errorType: metadata.terminalErrorType,
821
+ errorCode: metadata.terminalErrorCode,
822
+ });
823
+ },
824
+ });
825
+ }
826
+ catch (error) {
827
+ // Keep metadata available to app.onError, which records the client-facing
828
+ // failure with the same request ID before deleting the WeakMap entry.
829
+ finishActivity();
830
+ logProxyLifecycleEvent({
831
+ event: "request_terminal",
832
+ requestId: metadata.requestId,
833
+ method: metadata.method,
834
+ path: metadata.path,
835
+ model: metadata.model,
836
+ stream: metadata.stream,
837
+ toolCount: metadata.toolCount,
838
+ sessionHash,
839
+ requestBytes,
840
+ elapsedMs: performance.now() - startedMonotonicMs,
841
+ terminalOutcome: "handler_error",
842
+ errorType: error instanceof Error ? error.name : "unknown_error",
843
+ errorCode: getProxyRuntimeErrorCode(error),
844
+ });
845
+ throw error;
846
+ }
847
+ });
848
+ }
713
849
  export async function createProxyStartApp(params) {
714
850
  const { createClaudeProxyRoutes } = await import("../../lib/server/routes/claudeProxyRoutes.js");
715
851
  const { createOpenAIProxyRoutes } = await import("../../lib/server/routes/openaiProxyRoutes.js");
@@ -719,7 +855,9 @@ export async function createProxyStartApp(params) {
719
855
  const app = new Hono();
720
856
  const readiness = createProxyReadinessState();
721
857
  const requestMetadata = new WeakMap();
722
- const recordRuntimeError = async (metadata, status, errorType, errorMessage, clientMessage = errorMessage, clientErrorType = errorType) => {
858
+ const recordRuntimeError = async (metadata, status, errorType, errorMessage, options) => {
859
+ const clientMessage = options?.clientMessage ?? errorMessage;
860
+ const clientErrorType = options?.clientErrorType ?? errorType;
723
861
  recordFinalError(status);
724
862
  await Promise.all([
725
863
  logRequest({
@@ -736,6 +874,7 @@ export async function createProxyStartApp(params) {
736
874
  responseTimeMs: Date.now() - metadata.startedAt,
737
875
  errorType,
738
876
  errorMessage,
877
+ ...(options?.errorCode ? { errorCode: options.errorCode } : {}),
739
878
  }),
740
879
  logBodyCapture({
741
880
  timestamp: new Date().toISOString(),
@@ -769,7 +908,13 @@ export async function createProxyStartApp(params) {
769
908
  stream: false,
770
909
  toolCount: 0,
771
910
  };
772
- await recordRuntimeError(metadata, 502, "unhandled_proxy_error", errMsg, "Proxy internal error", "api_error");
911
+ metadata.terminalErrorType = "unhandled_proxy_error";
912
+ metadata.terminalErrorCode = getProxyRuntimeErrorCode(err);
913
+ await recordRuntimeError(metadata, 502, "unhandled_proxy_error", errMsg, {
914
+ clientMessage: "Proxy internal error",
915
+ clientErrorType: "api_error",
916
+ errorCode: metadata.terminalErrorCode,
917
+ });
773
918
  requestMetadata.delete(c.req.raw);
774
919
  return c.json({
775
920
  type: "error",
@@ -779,31 +924,7 @@ export async function createProxyStartApp(params) {
779
924
  },
780
925
  }, 502);
781
926
  });
782
- app.use("/v1/*", async (c, next) => {
783
- const metadata = {
784
- requestId: crypto.randomUUID(),
785
- method: c.req.method,
786
- path: c.req.path,
787
- startedAt: Date.now(),
788
- model: "-",
789
- stream: false,
790
- toolCount: 0,
791
- };
792
- requestMetadata.set(c.req.raw, metadata);
793
- const finishActivity = beginProxyRequest();
794
- const finish = () => {
795
- finishActivity();
796
- requestMetadata.delete(c.req.raw);
797
- };
798
- try {
799
- await next();
800
- c.res = trackProxyResponse(c.res, finish);
801
- }
802
- catch (error) {
803
- finishActivity();
804
- throw error;
805
- }
806
- });
927
+ registerProxyRequestTracking(app, requestMetadata);
807
928
  const runtimeConfigStore = params.runtimeConfigStore;
808
929
  const runtimeConfigProvider = runtimeConfigStore
809
930
  ? () => runtimeConfigStore.getSnapshot()
@@ -1042,6 +1163,9 @@ export async function createProxyStartApp(params) {
1042
1163
  lastActivityAt: activity.lastActivityAt?.toISOString() ?? null,
1043
1164
  };
1044
1165
  })(),
1166
+ observability: {
1167
+ lifecycle: getProxyLifecycleLoggerSnapshot(),
1168
+ },
1045
1169
  autoUpdate: {
1046
1170
  enabled: isProxyAutoUpdateEnabled(),
1047
1171
  updaterPid: runtimeState?.updaterPid ?? null,
@@ -1339,6 +1463,12 @@ function registerProxyShutdownHandlers(params) {
1339
1463
  exitCode = 1;
1340
1464
  logger.error(`[proxy] failed to drain server during shutdown: ${error instanceof Error ? error.message : String(error)}`);
1341
1465
  }
1466
+ try {
1467
+ await withTimeout(flushProxyLifecycleEvents(), PROXY_LIFECYCLE_SHUTDOWN_TIMEOUT_MS, "Timed out flushing proxy lifecycle metadata during shutdown");
1468
+ }
1469
+ catch (error) {
1470
+ logger.debug(`[proxy] lifecycle metadata flush failed during shutdown: ${error instanceof Error ? error.message : String(error)}`);
1471
+ }
1342
1472
  try {
1343
1473
  const { flushOpenTelemetry, shutdownOpenTelemetry } = await import("../../lib/services/server/ai/observability/instrumentation.js");
1344
1474
  await flushOpenTelemetry();
@@ -1,8 +1,8 @@
1
- import type { ProxyActivitySnapshot } from "../types/index.js";
1
+ import type { ProxyActivitySnapshot, ProxyResponseTrackingObserver } from "../types/index.js";
2
2
  /** Track one client-facing proxy request until its response body settles. */
3
3
  export declare function beginProxyRequest(): () => void;
4
4
  export declare function getProxyActivitySnapshot(): ProxyActivitySnapshot;
5
5
  export declare function isProxyActivityQuiet(snapshot: ProxyActivitySnapshot, quietThresholdMs: number, nowMs?: number): boolean;
6
6
  /** Keep activity open until the response body completes, errors, or is cancelled. */
7
- export declare function trackProxyResponse(response: Response, finishRequest: () => void): Response;
7
+ export declare function trackProxyResponse(response: Response, finishRequest: () => void, observer?: ProxyResponseTrackingObserver): Response;
8
8
  export declare function resetProxyActivityForTests(): void;
@@ -1,8 +1,24 @@
1
+ import { withTimeout } from "../utils/async/withTimeout.js";
2
+ import { logger } from "../utils/logger.js";
3
+ const PROXY_RESPONSE_CANCEL_TIMEOUT_MS = 1_000;
1
4
  let activeRequests = 0;
2
5
  let lastActivityAtMs = null;
3
6
  function touchActivity() {
4
7
  lastActivityAtMs = Date.now();
5
8
  }
9
+ function safelyNotifyObserver(callback) {
10
+ try {
11
+ callback?.();
12
+ }
13
+ catch (error) {
14
+ // Observability must never alter response handling.
15
+ if (logger.shouldLog("debug")) {
16
+ logger.debug("[proxy] response lifecycle observer failed", {
17
+ error: error instanceof Error ? error.message : String(error),
18
+ });
19
+ }
20
+ }
21
+ }
6
22
  /** Track one client-facing proxy request until its response body settles. */
7
23
  export function beginProxyRequest() {
8
24
  activeRequests += 1;
@@ -33,35 +49,59 @@ export function isProxyActivityQuiet(snapshot, quietThresholdMs, nowMs = Date.no
33
49
  return nowMs - snapshot.lastActivityAt.getTime() >= quietThresholdMs;
34
50
  }
35
51
  /** Keep activity open until the response body completes, errors, or is cancelled. */
36
- export function trackProxyResponse(response, finishRequest) {
52
+ export function trackProxyResponse(response, finishRequest, observer) {
37
53
  if (!response.body) {
38
54
  finishRequest();
55
+ safelyNotifyObserver(() => observer?.onTerminal?.({
56
+ outcome: "bodyless",
57
+ observedBodyBytes: 0,
58
+ responseChunks: 0,
59
+ }));
39
60
  return response;
40
61
  }
41
62
  const reader = response.body.getReader();
63
+ let observedBodyBytes = 0;
64
+ let responseChunks = 0;
65
+ let settled = false;
66
+ const settle = (outcome) => {
67
+ if (settled) {
68
+ return;
69
+ }
70
+ settled = true;
71
+ finishRequest();
72
+ safelyNotifyObserver(() => observer?.onTerminal?.({
73
+ outcome,
74
+ observedBodyBytes,
75
+ responseChunks,
76
+ }));
77
+ };
42
78
  const trackedBody = new ReadableStream({
43
79
  async pull(controller) {
44
80
  try {
45
81
  const { value, done } = await reader.read();
46
82
  if (done) {
47
- finishRequest();
83
+ settle("completed");
48
84
  controller.close();
49
85
  return;
50
86
  }
51
87
  controller.enqueue(value);
88
+ observedBodyBytes += value.byteLength;
89
+ responseChunks += 1;
90
+ if (responseChunks === 1) {
91
+ safelyNotifyObserver(() => observer?.onFirstChunk?.({
92
+ observedBodyBytes,
93
+ responseChunks: 1,
94
+ }));
95
+ }
52
96
  }
53
97
  catch (error) {
54
- finishRequest();
98
+ settle("stream_error");
55
99
  controller.error(error);
56
100
  }
57
101
  },
58
102
  async cancel(reason) {
59
- try {
60
- await reader.cancel(reason);
61
- }
62
- finally {
63
- finishRequest();
64
- }
103
+ settle("client_cancelled");
104
+ await withTimeout(reader.cancel(reason), PROXY_RESPONSE_CANCEL_TIMEOUT_MS, "Timed out cancelling the upstream proxy response");
65
105
  },
66
106
  });
67
107
  return new Response(trackedBody, {
@@ -0,0 +1,8 @@
1
+ import type { ProxyLifecycleEventInput, ProxyLifecycleLoggerOptions, ProxyLifecycleLoggerSnapshot } from "../types/index.js";
2
+ export declare function hashProxyLifecycleSessionId(sessionId: string | undefined): string | undefined;
3
+ export declare function configureProxyLifecycleLogger(options: ProxyLifecycleLoggerOptions): void;
4
+ /** Enqueue fixed-size lifecycle metadata without awaiting filesystem work. */
5
+ export declare function logProxyLifecycleEvent(input: ProxyLifecycleEventInput): void;
6
+ export declare function flushProxyLifecycleEvents(): Promise<void>;
7
+ export declare function getProxyLifecycleLoggerSnapshot(): ProxyLifecycleLoggerSnapshot;
8
+ export declare function resetProxyLifecycleLoggerForTests(): void;