@juspay/neurolink 12.12.5 → 12.12.7

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.
@@ -26,7 +26,7 @@ import { ProxyRuntimeConfigStore } from "../../proxy/runtimeConfig.js";
26
26
  import { startProxyLogCleanupScheduler } from "../../proxy/logCleanupScheduler.js";
27
27
  import { anthropicAccountKeysEqual, createAccountAllowlist, isAccountAllowed, LEGACY_ANTHROPIC_ACCOUNT_KEY, normalizeAnthropicAccountKey, shouldLoadFallbackCredential, } from "../../proxy/accountSelection.js";
28
28
  import { resolveProxyStatusAccountIdentity } from "../../proxy/codexAccountUsage.js";
29
- import { beginProxyRequest, getProxyActivitySnapshot, takeProxyResponseObservers, trackProxyResponse, } from "../../proxy/proxyActivity.js";
29
+ import { beginProxyRequest, getProxyActivitySnapshot, observeProxyFinalLog, takeProxyResponseObservers, trackProxyResponse, } from "../../proxy/proxyActivity.js";
30
30
  import { flushProxyLifecycleEvents, getProxyLifecycleLoggerSnapshot, hashProxyLifecycleSessionId, logProxyLifecycleEvent, } from "../../proxy/proxyLifecycle.js";
31
31
  import { describeInstallFailure, getGlobalInstallArgs, isTransientInstallFailure, resolveGlobalInstaller, validateInstalledVersion, } from "../../proxy/globalInstaller.js";
32
32
  import { startUpdaterWorkerSupervisor } from "../../proxy/updaterSupervisor.js";
@@ -1079,10 +1079,14 @@ function registerProxyRequestTracking(app, requestMetadata, readiness) {
1079
1079
  rejectForUpdate: readiness.drainingForUpdate,
1080
1080
  };
1081
1081
  requestMetadata.set(c.req.raw, metadata);
1082
+ const stopObservingFinalLog = observeProxyFinalLog(metadata.requestId, (entry) => {
1083
+ metadata.terminalResult = entry;
1084
+ });
1082
1085
  const finishActivity = metadata.rejectForUpdate
1083
1086
  ? () => undefined
1084
1087
  : beginProxyRequest();
1085
1088
  const finish = () => {
1089
+ stopObservingFinalLog();
1086
1090
  finishActivity();
1087
1091
  // Borrowed traffic holds a concurrency slot for the lifetime of the
1088
1092
  // response body, so it is released here rather than when the handler
@@ -1130,15 +1134,9 @@ function registerProxyRequestTracking(app, requestMetadata, readiness) {
1130
1134
  }
1131
1135
  }
1132
1136
  };
1133
- const notifyRouteTerminal = (details) => {
1134
- for (const observer of routeResponseObservers) {
1135
- try {
1136
- observer.onTerminal?.(details);
1137
- }
1138
- catch {
1139
- // Route-level accounting must never interfere with the relay.
1140
- }
1141
- }
1137
+ const notifyRouteTerminal = async (details) => {
1138
+ const results = await withTimeout(Promise.allSettled(routeResponseObservers.map(async (observer) => observer.onTerminal?.(details))), 2_000, "Timed out joining proxy response accounting");
1139
+ return results.some((result) => result.status === "rejected");
1142
1140
  };
1143
1141
  c.res = trackProxyResponse(c.res, finish, {
1144
1142
  onFirstChunk: ({ observedBodyBytes, responseChunks }) => {
@@ -1162,13 +1160,39 @@ function registerProxyRequestTracking(app, requestMetadata, readiness) {
1162
1160
  responseChunks,
1163
1161
  });
1164
1162
  },
1165
- onTerminal: ({ outcome, observedBodyBytes, responseChunks }) => {
1166
- if (outcome === "completed" &&
1167
- metadata.terminalErrorType === "stream_error") {
1168
- outcome = "stream_error";
1163
+ onTerminal: async ({ outcome, error, observedBodyBytes, responseChunks, }) => {
1164
+ const terminalMonotonicMs = performance.now();
1165
+ const terminalTimestampMs = Date.now();
1166
+ // Route accounting may await SSE parsing/cancellation. Join it before
1167
+ // publishing the semantic terminal record; transport EOF alone is not
1168
+ // evidence of a successful model response.
1169
+ let accountingTimedOut = false;
1170
+ let accountingFailed = false;
1171
+ try {
1172
+ accountingFailed = await notifyRouteTerminal({
1173
+ outcome,
1174
+ error,
1175
+ observedBodyBytes,
1176
+ responseChunks,
1177
+ });
1178
+ }
1179
+ catch {
1180
+ accountingTimedOut = true;
1169
1181
  }
1182
+ const final = metadata.terminalResult;
1183
+ const terminalOutcome = final?.terminalOutcome ??
1184
+ (outcome === "stream_error" ||
1185
+ metadata.terminalErrorType === "stream_error"
1186
+ ? "stream_error"
1187
+ : outcome === "client_cancelled"
1188
+ ? "client_cancelled"
1189
+ : responseStatus >= 400
1190
+ ? "handler_error"
1191
+ : "unknown");
1170
1192
  logProxyLifecycleEvent({
1171
1193
  event: "request_terminal",
1194
+ timestampMs: terminalTimestampMs,
1195
+ monotonicMs: terminalMonotonicMs,
1172
1196
  requestId: metadata.requestId,
1173
1197
  method: metadata.method,
1174
1198
  path: metadata.path,
@@ -1180,20 +1204,33 @@ function registerProxyRequestTracking(app, requestMetadata, readiness) {
1180
1204
  responseStatus,
1181
1205
  observedBodyBytes,
1182
1206
  responseChunks,
1183
- elapsedMs: performance.now() - startedMonotonicMs,
1184
- terminalOutcome: outcome,
1185
- errorType: metadata.terminalErrorType,
1186
- errorCode: metadata.terminalErrorCode,
1187
- });
1188
- notifyRouteTerminal({
1189
- outcome,
1190
- observedBodyBytes,
1191
- responseChunks,
1207
+ elapsedMs: terminalMonotonicMs - startedMonotonicMs,
1208
+ terminalOutcome,
1209
+ finalStatus: final?.responseStatus,
1210
+ transportOutcome: outcome,
1211
+ outcomeSource: final
1212
+ ? "final_request"
1213
+ : responseStatus >= 400
1214
+ ? "http_status"
1215
+ : terminalOutcome === "unknown"
1216
+ ? "unknown"
1217
+ : "transport_error",
1218
+ telemetryStatus: accountingTimedOut
1219
+ ? "timeout"
1220
+ : accountingFailed
1221
+ ? "observer_error"
1222
+ : final
1223
+ ? "complete"
1224
+ : "missing_final",
1225
+ errorType: final?.errorType ?? metadata.terminalErrorType,
1226
+ errorCode: final?.errorCode ?? metadata.terminalErrorCode,
1192
1227
  });
1228
+ stopObservingFinalLog();
1193
1229
  },
1194
1230
  });
1195
1231
  }
1196
1232
  catch (error) {
1233
+ stopObservingFinalLog();
1197
1234
  // Keep metadata available to app.onError, which records the client-facing
1198
1235
  // failure with the same request ID before deleting the WeakMap entry.
1199
1236
  finishActivity();
@@ -1238,7 +1275,7 @@ export async function createProxyStartApp(params) {
1238
1275
  const { createOpenAIProxyRoutes } = await import("../../server/routes/openaiProxyRoutes.js");
1239
1276
  const { createCodexProxyRoutes } = await import("../../server/routes/codexProxyRoutes.js");
1240
1277
  const { createGeminiProxyRoutes } = await import("../../server/routes/geminiProxyRoutes.js");
1241
- const { logBodyCapture, logRequest } = await import("../../proxy/requestLogger.js");
1278
+ const { logBodyCapture, logRequest, getRequestLoggerSnapshot } = await import("../../proxy/requestLogger.js");
1242
1279
  const { recordFinalError } = await import("../../proxy/usageStats.js");
1243
1280
  const { admitInboundShareRequest, isGrantRequiredByEnv } = await import("../../proxy/shareGate.js");
1244
1281
  const { runWithShareContext } = await import("../../proxy/shareContext.js");
@@ -1978,6 +2015,7 @@ export async function createProxyStartApp(params) {
1978
2015
  })(),
1979
2016
  observability: {
1980
2017
  lifecycle: getProxyLifecycleLoggerSnapshot(),
2018
+ requestLogs: getRequestLoggerSnapshot(),
1981
2019
  },
1982
2020
  autoUpdate: {
1983
2021
  enabled: isProxyAutoUpdateEnabled(),
@@ -70,6 +70,7 @@ function printAnalysis(report) {
70
70
  for (const [label, summary] of [
71
71
  ["Response headers", report.latencyMs.headers],
72
72
  ["First chunk", report.latencyMs.firstChunk],
73
+ ["First useful output", report.latencyMs.firstUsefulOutput],
73
74
  ["Terminal", report.latencyMs.terminal],
74
75
  ["Final request log", report.latencyMs.finalRequest],
75
76
  ["Account attempt", report.latencyMs.attempt],
@@ -100,7 +101,9 @@ function printAnalysis(report) {
100
101
  if (!report.coverage.comparableRequestAttempts) {
101
102
  logger.always(chalk.yellow(" WARNING: request and attempt totals do not cover a comparable full window; do not reconcile them as one cohort"));
102
103
  }
103
- logger.always(` ${report.dataQuality.linesRead} lines scanned, ${report.dataQuality.malformedLines} malformed, ${report.dataQuality.unsupportedLifecycleLines} unsupported lifecycle, ${report.dataQuality.lifecycleSequenceGaps} sequence gaps, ${report.dataQuality.lifecycleSequenceDuplicates} duplicates`);
104
+ logger.always(` ${report.dataQuality.linesRead} lines scanned, ${report.dataQuality.malformedLines} malformed, ${report.dataQuality.unsupportedLifecycleLines} unsupported lifecycle, ${report.dataQuality.lifecycleSequenceGaps} sequence gaps, ${report.dataQuality.lifecycleSequenceDuplicates} duplicates (${report.dataQuality.conflictingLifecycleDuplicates} conflicting)`);
105
+ logger.always(` Outcome evidence: ${report.dataQuality.finalOutcomeConflicts} conflicts reconciled, ${report.dataQuality.acceptedWithoutFinal} accepted without a final record, ${report.dataQuality.terminalWithoutFinal} transport terminals without a final record`);
106
+ logger.always(` Repeated attempt records merged: ${report.dataQuality.duplicateAttempts}`);
104
107
  logger.always(` Routing decisions: ${report.dataQuality.routingDecisions.valid} valid, ${report.dataQuality.routingDecisions.invalid} invalid, ${report.dataQuality.routingDecisions.absent} absent`);
105
108
  for (const [stream, range] of Object.entries(report.dataQuality.streams)) {
106
109
  if (range.observedFrom) {
@@ -468,6 +468,16 @@ export declare abstract class BaseProvider implements AIProvider {
468
468
  * TODO(#1576): Implement global level middlewares that can be used
469
469
  */
470
470
  protected getAISDKModelWithMiddleware(options?: TextGenerationOptions | StreamOptions): Promise<LanguageModel>;
471
+ /**
472
+ * Apply the configured middleware chain to a caller-supplied base model.
473
+ *
474
+ * `getAISDKModelWithMiddleware()` always wraps `getAISDKModel()`, which is
475
+ * the model the non-streaming path drives. Streaming paths build a
476
+ * different base — one whose `doStream` starts the provider's own stream
477
+ * loop — and need the same chain applied to it, so the wrapping is split
478
+ * out here rather than duplicated per provider.
479
+ */
480
+ protected applyMiddlewareToModel(baseModel: LanguageModel, options?: TextGenerationOptions | StreamOptions): Promise<LanguageModel>;
471
481
  /**
472
482
  * Extract middleware options - delegated to Utilities
473
483
  */
@@ -2014,8 +2014,18 @@ export class BaseProvider {
2014
2014
  * TODO(#1576): Implement global level middlewares that can be used
2015
2015
  */
2016
2016
  async getAISDKModelWithMiddleware(options = {}) {
2017
- // Get the base model
2018
- const baseModel = await this.getAISDKModel();
2017
+ return this.applyMiddlewareToModel(await this.getAISDKModel(), options);
2018
+ }
2019
+ /**
2020
+ * Apply the configured middleware chain to a caller-supplied base model.
2021
+ *
2022
+ * `getAISDKModelWithMiddleware()` always wraps `getAISDKModel()`, which is
2023
+ * the model the non-streaming path drives. Streaming paths build a
2024
+ * different base — one whose `doStream` starts the provider's own stream
2025
+ * loop — and need the same chain applied to it, so the wrapping is split
2026
+ * out here rather than duplicated per provider.
2027
+ */
2028
+ async applyMiddlewareToModel(baseModel, options = {}) {
2019
2029
  logger.debug(`Retrieved base model for ${this.providerName}`, {
2020
2030
  provider: this.providerName,
2021
2031
  model: this.modelName,
@@ -43,7 +43,6 @@ export function createGuardrailsMiddleware(config = {}) {
43
43
  const blockingState = new WeakMap();
44
44
  const middleware = {
45
45
  specificationVersion: "v3",
46
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
47
46
  transformParams: async ({ params }) => {
48
47
  if (config.precallEvaluation?.enabled) {
49
48
  const { shouldBlock, transformedParams } = await handlePrecallGuardrails(params, config.precallEvaluation);
@@ -53,7 +52,6 @@ export function createGuardrailsMiddleware(config = {}) {
53
52
  }
54
53
  return params;
55
54
  },
56
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
57
55
  wrapGenerate: async ({ doGenerate, params }) => {
58
56
  logger.debug(`[GuardrailsMiddleware] Applying to generate call.`);
59
57
  // Check if this request should be blocked (set by transformParams)
@@ -61,14 +59,23 @@ export function createGuardrailsMiddleware(config = {}) {
61
59
  return createBlockedResponse();
62
60
  }
63
61
  let result = await doGenerate();
64
- const filterResult = applyContentFiltering(result.text || "", config.badWords, "generate");
65
- if (filterResult.hasChanges) {
66
- result = { ...result, text: filterResult.filteredText };
67
- }
62
+ result = {
63
+ ...result,
64
+ content: result.content.map((part) => part.type === "text"
65
+ ? {
66
+ ...part,
67
+ text: applyContentFiltering(part.text, config.badWords, "generate").filteredText,
68
+ }
69
+ : part),
70
+ };
68
71
  if (config.modelFilter?.enabled && config.modelFilter.filterModel) {
69
72
  logger.debug(`[GuardrailsMiddleware] Invoking model-based filter.`);
70
73
  try {
71
- const filterPrompt = `Is the following text safe? Respond with only "safe" or "unsafe".\n\nText: "${result.text}"`;
74
+ const text = result.content
75
+ .filter((part) => part.type === "text")
76
+ .map((part) => part.text)
77
+ .join("");
78
+ const filterPrompt = `Is the following text safe? Respond with only "safe" or "unsafe".\n\nText: "${text}"`;
72
79
  // `ModelFilterConfig.filterModel` is typed `LanguageModel`, which
73
80
  // admits a bare model id, and the documented examples used one.
74
81
  // `generateOnceNative` needs a handle exposing doGenerate, so a
@@ -78,7 +85,12 @@ export function createGuardrailsMiddleware(config = {}) {
78
85
  const { text: filterResponse } = await generateOnceNative(filterModel, { prompt: filterPrompt });
79
86
  if (filterResponse.toLowerCase().trim() === "unsafe") {
80
87
  logger.warn(`[GuardrailsMiddleware] Model-based filter flagged content as unsafe.`);
81
- result = { ...result, text: "<REDACTED BY AI GUARDRAIL>" };
88
+ result = {
89
+ ...result,
90
+ content: result.content.map((part) => part.type === "text"
91
+ ? { ...part, text: "<REDACTED BY AI GUARDRAIL>" }
92
+ : part),
93
+ };
82
94
  }
83
95
  }
84
96
  catch (error) {
@@ -89,7 +101,6 @@ export function createGuardrailsMiddleware(config = {}) {
89
101
  }
90
102
  return result;
91
103
  },
92
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
93
104
  wrapStream: async ({ doStream, params }) => {
94
105
  logger.debug(`[GuardrailsMiddleware] Applying to stream call.`);
95
106
  // Check if this request should be blocked (set by transformParams)
@@ -106,13 +117,12 @@ export function createGuardrailsMiddleware(config = {}) {
106
117
  transform(chunk, controller) {
107
118
  hasYieldedChunks = true;
108
119
  let filteredChunk = chunk;
109
- if (typeof filteredChunk === "object" &&
110
- "textDelta" in filteredChunk) {
111
- const filterResult = applyContentFiltering(filteredChunk.textDelta, config.badWords, "stream");
120
+ if (filteredChunk.type === "text-delta") {
121
+ const filterResult = applyContentFiltering(filteredChunk.delta, config.badWords, "stream");
112
122
  if (filterResult.hasChanges) {
113
123
  filteredChunk = {
114
124
  ...filteredChunk,
115
- textDelta: filterResult.filteredText,
125
+ delta: filterResult.filteredText,
116
126
  };
117
127
  }
118
128
  }
@@ -1,4 +1,4 @@
1
- import type { BadWordsConfig, ContentFilteringResult, EvaluationActionResult, PrecallEvaluationConfig, PrecallEvaluationResult } from "../../types/index.js";
1
+ import type { BadWordsConfig, LanguageModelV3GenerateResult, LanguageModelV3StreamPart, ContentFilteringResult, EvaluationActionResult, PrecallEvaluationConfig, PrecallEvaluationResult } from "../../types/index.js";
2
2
  export declare const DEFAULT_EVALUATION_PROMPT = "\nEvaluate the following user input for safety and appropriateness. Respond ONLY with valid JSON:\n\n{\n \"overall\": \"safe|unsafe|suspicious|inappropriate\",\n \"safetyScore\": <1-10>,\n \"appropriatenessScore\": <1-10>,\n \"confidenceLevel\": <1-10>,\n \"issues\": [\n {\n \"category\": \"explicit_content|hate_speech|violence|personal_info|spam|other\",\n \"severity\": \"low|medium|high|critical\",\n \"description\": \"Brief description\"\n }\n ],\n \"suggestedAction\": \"allow|block|sanitize|warn\",\n \"reasoning\": \"Brief explanation\"\n}\n\nUser Input: \"{USER_INPUT}\"\n";
3
3
  /**
4
4
  * Extract user input from middleware params
@@ -25,20 +25,8 @@ export declare function applyEvaluationActions(evaluation: PrecallEvaluationResu
25
25
  */
26
26
  export declare function applySanitization(params: any, sanitizedInput: string): any;
27
27
  export declare function escapeRegExp(string: string): string;
28
- export declare function createBlockedResponse(): {
29
- text: string;
30
- usage: {
31
- promptTokens: number;
32
- completionTokens: number;
33
- };
34
- finishReason: "stop";
35
- warnings: never[];
36
- rawCall: {
37
- rawPrompt: null;
38
- rawSettings: {};
39
- };
40
- };
41
- export declare function createBlockedStream(): ReadableStream<any>;
28
+ export declare function createBlockedResponse(): LanguageModelV3GenerateResult;
29
+ export declare function createBlockedStream(): ReadableStream<LanguageModelV3StreamPart>;
42
30
  /**
43
31
  * Apply content filtering using bad words configuration
44
32
  * Handles both regex patterns and string lists with proper priority
@@ -258,24 +258,31 @@ export function escapeRegExp(string) {
258
258
  }
259
259
  export function createBlockedResponse() {
260
260
  return {
261
- text: "Request contains inappropriate content and has been blocked.",
262
- usage: { promptTokens: 0, completionTokens: 0 },
263
- finishReason: "stop",
261
+ content: [
262
+ {
263
+ type: "text",
264
+ text: "Request contains inappropriate content and has been blocked.",
265
+ },
266
+ ],
267
+ usage: { inputTokens: { total: 0 }, outputTokens: { total: 0 } },
268
+ finishReason: { unified: "stop" },
264
269
  warnings: [],
265
- rawCall: { rawPrompt: null, rawSettings: {} },
266
270
  };
267
271
  }
268
272
  export function createBlockedStream() {
269
273
  return new ReadableStream({
270
274
  start(controller) {
275
+ controller.enqueue({ type: "text-start", id: "blocked" });
271
276
  controller.enqueue({
272
277
  type: "text-delta",
273
- textDelta: "Request contains inappropriate content and has been blocked.",
278
+ id: "blocked",
279
+ delta: "Request contains inappropriate content and has been blocked.",
274
280
  });
281
+ controller.enqueue({ type: "text-end", id: "blocked" });
275
282
  controller.enqueue({
276
283
  type: "finish",
277
- finishReason: "stop",
278
- usage: { promptTokens: 0, completionTokens: 0 },
284
+ finishReason: { unified: "stop" },
285
+ usage: { inputTokens: { total: 0 }, outputTokens: { total: 0 } },
279
286
  });
280
287
  controller.close();
281
288
  },
@@ -6,10 +6,9 @@
6
6
  * `wrapGenerate` / `wrapStream` hooks. Reproduced here so the middleware
7
7
  * factory no longer needs the ai package.
8
8
  *
9
- * Worth recording: `wrapStream` does not currently run in this codebase. Every
10
- * streaming path is native and bypasses the wrapped model entirely, so only
11
- * `wrapGenerate` is reachable. That is a pre-existing gap, not one this
12
- * introduced.
9
+ * The OpenAI-compatible streaming path also uses this wrapper. Other native
10
+ * streaming implementations must opt in explicitly; exposing a middleware
11
+ * option or a model-shaped handle alone does not apply the chain.
13
12
  */
14
13
  import type { LanguageModelV3, LanguageModelV3Middleware } from "../types/index.js";
15
14
  export declare const wrapLanguageModel: ({ model, middleware, }: {
@@ -6,10 +6,9 @@
6
6
  * `wrapGenerate` / `wrapStream` hooks. Reproduced here so the middleware
7
7
  * factory no longer needs the ai package.
8
8
  *
9
- * Worth recording: `wrapStream` does not currently run in this codebase. Every
10
- * streaming path is native and bypasses the wrapped model entirely, so only
11
- * `wrapGenerate` is reachable. That is a pre-existing gap, not one this
12
- * introduced.
9
+ * The OpenAI-compatible streaming path also uses this wrapper. Other native
10
+ * streaming implementations must opt in explicitly; exposing a middleware
11
+ * option or a model-shaped handle alone does not apply the chain.
13
12
  */
14
13
  const doWrap = (model, middleware) => {
15
14
  const transform = async (params, type) => middleware.transformParams