@juspay/neurolink 9.25.0 → 9.25.2

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 CHANGED
@@ -1,3 +1,15 @@
1
+ ## [9.25.2](https://github.com/juspay/neurolink/compare/v9.25.1...v9.25.2) (2026-03-15)
2
+
3
+ ### Bug Fixes
4
+
5
+ - **(core):** remove hardcoded Ollama model and unsafe type assertions (BZ-463) ([f12e03b](https://github.com/juspay/neurolink/commit/f12e03bfef007464d27c36b44694c5c4c799c6ea))
6
+
7
+ ## [9.25.1](https://github.com/juspay/neurolink/compare/v9.25.0...v9.25.1) (2026-03-15)
8
+
9
+ ### Bug Fixes
10
+
11
+ - **(observability):** address code review findings from PR [#860](https://github.com/juspay/neurolink/issues/860) ([1655c7e](https://github.com/juspay/neurolink/commit/1655c7ef6991eb104834e10fa7d21516539ea16d))
12
+
1
13
  ## [9.25.0](https://github.com/juspay/neurolink/compare/v9.24.0...v9.25.0) (2026-03-15)
2
14
 
3
15
  ### Features
@@ -3,13 +3,9 @@ import { generateText } from "ai";
3
3
  import { directAgentTools } from "../agent/directTools.js";
4
4
  import { IMAGE_GENERATION_MODELS } from "../core/constants.js";
5
5
  import { MiddlewareFactory } from "../middleware/factory.js";
6
- import { getMetricsAggregator } from "../observability/metricsAggregator.js";
7
- import { SpanStatus, SpanType } from "../observability/types/spanTypes.js";
8
- import { SpanSerializer } from "../observability/utils/spanSerializer.js";
9
6
  import { ATTR, tracers } from "../telemetry/index.js";
10
7
  import { isAbortError } from "../utils/errorHandling.js";
11
8
  import { logger } from "../utils/logger.js";
12
- import { calculateCost } from "../utils/pricing.js";
13
9
  import { composeAbortSignals, createTimeoutController, TimeoutError, } from "../utils/timeout.js";
14
10
  import { shouldDisableBuiltinTools } from "../utils/toolUtils.js";
15
11
  import { getKeyCount, getKeysAsString } from "../utils/transformationUtils.js";
@@ -85,14 +81,6 @@ export class BaseProvider {
85
81
  */
86
82
  async stream(optionsOrPrompt, analysisSchema) {
87
83
  let options = this.normalizeStreamOptions(optionsOrPrompt);
88
- // Observability: create metrics span for provider.stream
89
- const metricsSpan = SpanSerializer.createSpan(SpanType.MODEL_GENERATION, "provider.stream", {
90
- "ai.provider": this.providerName || "unknown",
91
- "ai.model": this.modelName || options.model || "unknown",
92
- "ai.temperature": options.temperature,
93
- "ai.max_tokens": options.maxTokens,
94
- }, this._traceContext?.parentSpanId, this._traceContext?.traceId);
95
- let metricsSpanRecorded = false;
96
84
  // OTEL span for provider-level stream tracing
97
85
  const otelStreamSpan = tracers.provider.startSpan("neurolink.provider.stream", {
98
86
  kind: SpanKind.CLIENT,
@@ -185,10 +173,6 @@ export class BaseProvider {
185
173
  }
186
174
  }
187
175
  catch (error) {
188
- // Observability: record failed stream span
189
- metricsSpanRecorded = true;
190
- const endedStreamSpan = SpanSerializer.endSpan(metricsSpan, SpanStatus.ERROR, error instanceof Error ? error.message : String(error));
191
- getMetricsAggregator().recordSpan(endedStreamSpan);
192
176
  otelStreamSpan.setStatus({
193
177
  code: SpanStatusCode.ERROR,
194
178
  message: error instanceof Error ? error.message : String(error),
@@ -197,10 +181,8 @@ export class BaseProvider {
197
181
  throw error;
198
182
  }
199
183
  finally {
200
- // Observability: record successful stream span (only if not already ended via error path)
201
- if (!metricsSpanRecorded) {
202
- const endedStreamSpan = SpanSerializer.endSpan(metricsSpan, SpanStatus.OK);
203
- getMetricsAggregator().recordSpan(endedStreamSpan);
184
+ // End OTEL span on success (only if not already ended via error path)
185
+ if (otelStreamSpan.isRecording()) {
204
186
  otelStreamSpan.setStatus({ code: SpanStatusCode.OK });
205
187
  otelStreamSpan.end();
206
188
  }
@@ -495,13 +477,6 @@ export class BaseProvider {
495
477
  const options = this.normalizeTextOptions(optionsOrPrompt);
496
478
  this.validateOptions(options);
497
479
  const startTime = Date.now();
498
- // Observability: create metrics span for provider.generate
499
- const metricsSpan = SpanSerializer.createSpan(SpanType.MODEL_GENERATION, "provider.generate", {
500
- "ai.provider": this.providerName || "unknown",
501
- "ai.model": this.modelName || options.model || "unknown",
502
- "ai.temperature": options.temperature,
503
- "ai.max_tokens": options.maxTokens,
504
- }, this._traceContext?.parentSpanId, this._traceContext?.traceId);
505
480
  // OTEL span for provider-level generate tracing
506
481
  // Use startActiveSpan pattern via context.with() so child spans become descendants
507
482
  const otelSpan = tracers.provider.startSpan("neurolink.provider.generate", {
@@ -690,33 +665,9 @@ export class BaseProvider {
690
665
  });
691
666
  }
692
667
  }
693
- // Observability: record successful generate span with token/cost data
694
- let enrichedGenerateSpan = { ...metricsSpan };
695
- if (enhancedResult?.usage) {
696
- enrichedGenerateSpan = SpanSerializer.enrichWithTokenUsage(enrichedGenerateSpan, {
697
- promptTokens: enhancedResult.usage.input || 0,
698
- completionTokens: enhancedResult.usage.output || 0,
699
- totalTokens: enhancedResult.usage.total || 0,
700
- });
701
- const cost = calculateCost(this.providerName, this.modelName, {
702
- input: enhancedResult.usage.input || 0,
703
- output: enhancedResult.usage.output || 0,
704
- total: enhancedResult.usage.total || 0,
705
- });
706
- if (cost && cost > 0) {
707
- enrichedGenerateSpan = SpanSerializer.enrichWithCost(enrichedGenerateSpan, {
708
- totalCost: cost,
709
- });
710
- }
711
- }
712
- const endedGenerateSpan = SpanSerializer.endSpan(enrichedGenerateSpan, SpanStatus.OK);
713
- getMetricsAggregator().recordSpan(endedGenerateSpan);
714
668
  return await this.enhanceResult(enhancedResult, options, startTime);
715
669
  }
716
670
  catch (error) {
717
- // Observability: record failed generate span
718
- const endedGenerateSpan = SpanSerializer.endSpan(metricsSpan, SpanStatus.ERROR, error instanceof Error ? error.message : String(error));
719
- getMetricsAggregator().recordSpan(endedGenerateSpan);
720
671
  otelSpan.setStatus({
721
672
  code: SpanStatusCode.ERROR,
722
673
  message: error instanceof Error ? error.message : String(error),
@@ -3,13 +3,9 @@ import { generateText } from "ai";
3
3
  import { directAgentTools } from "../agent/directTools.js";
4
4
  import { IMAGE_GENERATION_MODELS } from "../core/constants.js";
5
5
  import { MiddlewareFactory } from "../middleware/factory.js";
6
- import { getMetricsAggregator } from "../observability/metricsAggregator.js";
7
- import { SpanStatus, SpanType } from "../observability/types/spanTypes.js";
8
- import { SpanSerializer } from "../observability/utils/spanSerializer.js";
9
6
  import { ATTR, tracers } from "../telemetry/index.js";
10
7
  import { isAbortError } from "../utils/errorHandling.js";
11
8
  import { logger } from "../utils/logger.js";
12
- import { calculateCost } from "../utils/pricing.js";
13
9
  import { composeAbortSignals, createTimeoutController, TimeoutError, } from "../utils/timeout.js";
14
10
  import { shouldDisableBuiltinTools } from "../utils/toolUtils.js";
15
11
  import { getKeyCount, getKeysAsString } from "../utils/transformationUtils.js";
@@ -85,14 +81,6 @@ export class BaseProvider {
85
81
  */
86
82
  async stream(optionsOrPrompt, analysisSchema) {
87
83
  let options = this.normalizeStreamOptions(optionsOrPrompt);
88
- // Observability: create metrics span for provider.stream
89
- const metricsSpan = SpanSerializer.createSpan(SpanType.MODEL_GENERATION, "provider.stream", {
90
- "ai.provider": this.providerName || "unknown",
91
- "ai.model": this.modelName || options.model || "unknown",
92
- "ai.temperature": options.temperature,
93
- "ai.max_tokens": options.maxTokens,
94
- }, this._traceContext?.parentSpanId, this._traceContext?.traceId);
95
- let metricsSpanRecorded = false;
96
84
  // OTEL span for provider-level stream tracing
97
85
  const otelStreamSpan = tracers.provider.startSpan("neurolink.provider.stream", {
98
86
  kind: SpanKind.CLIENT,
@@ -185,10 +173,6 @@ export class BaseProvider {
185
173
  }
186
174
  }
187
175
  catch (error) {
188
- // Observability: record failed stream span
189
- metricsSpanRecorded = true;
190
- const endedStreamSpan = SpanSerializer.endSpan(metricsSpan, SpanStatus.ERROR, error instanceof Error ? error.message : String(error));
191
- getMetricsAggregator().recordSpan(endedStreamSpan);
192
176
  otelStreamSpan.setStatus({
193
177
  code: SpanStatusCode.ERROR,
194
178
  message: error instanceof Error ? error.message : String(error),
@@ -197,10 +181,8 @@ export class BaseProvider {
197
181
  throw error;
198
182
  }
199
183
  finally {
200
- // Observability: record successful stream span (only if not already ended via error path)
201
- if (!metricsSpanRecorded) {
202
- const endedStreamSpan = SpanSerializer.endSpan(metricsSpan, SpanStatus.OK);
203
- getMetricsAggregator().recordSpan(endedStreamSpan);
184
+ // End OTEL span on success (only if not already ended via error path)
185
+ if (otelStreamSpan.isRecording()) {
204
186
  otelStreamSpan.setStatus({ code: SpanStatusCode.OK });
205
187
  otelStreamSpan.end();
206
188
  }
@@ -495,13 +477,6 @@ export class BaseProvider {
495
477
  const options = this.normalizeTextOptions(optionsOrPrompt);
496
478
  this.validateOptions(options);
497
479
  const startTime = Date.now();
498
- // Observability: create metrics span for provider.generate
499
- const metricsSpan = SpanSerializer.createSpan(SpanType.MODEL_GENERATION, "provider.generate", {
500
- "ai.provider": this.providerName || "unknown",
501
- "ai.model": this.modelName || options.model || "unknown",
502
- "ai.temperature": options.temperature,
503
- "ai.max_tokens": options.maxTokens,
504
- }, this._traceContext?.parentSpanId, this._traceContext?.traceId);
505
480
  // OTEL span for provider-level generate tracing
506
481
  // Use startActiveSpan pattern via context.with() so child spans become descendants
507
482
  const otelSpan = tracers.provider.startSpan("neurolink.provider.generate", {
@@ -690,33 +665,9 @@ export class BaseProvider {
690
665
  });
691
666
  }
692
667
  }
693
- // Observability: record successful generate span with token/cost data
694
- let enrichedGenerateSpan = { ...metricsSpan };
695
- if (enhancedResult?.usage) {
696
- enrichedGenerateSpan = SpanSerializer.enrichWithTokenUsage(enrichedGenerateSpan, {
697
- promptTokens: enhancedResult.usage.input || 0,
698
- completionTokens: enhancedResult.usage.output || 0,
699
- totalTokens: enhancedResult.usage.total || 0,
700
- });
701
- const cost = calculateCost(this.providerName, this.modelName, {
702
- input: enhancedResult.usage.input || 0,
703
- output: enhancedResult.usage.output || 0,
704
- total: enhancedResult.usage.total || 0,
705
- });
706
- if (cost && cost > 0) {
707
- enrichedGenerateSpan = SpanSerializer.enrichWithCost(enrichedGenerateSpan, {
708
- totalCost: cost,
709
- });
710
- }
711
- }
712
- const endedGenerateSpan = SpanSerializer.endSpan(enrichedGenerateSpan, SpanStatus.OK);
713
- getMetricsAggregator().recordSpan(endedGenerateSpan);
714
668
  return await this.enhanceResult(enhancedResult, options, startTime);
715
669
  }
716
670
  catch (error) {
717
- // Observability: record failed generate span
718
- const endedGenerateSpan = SpanSerializer.endSpan(metricsSpan, SpanStatus.ERROR, error instanceof Error ? error.message : String(error));
719
- getMetricsAggregator().recordSpan(endedGenerateSpan);
720
671
  otelSpan.setStatus({
721
672
  code: SpanStatusCode.ERROR,
722
673
  message: error instanceof Error ? error.message : String(error),
@@ -1263,18 +1263,42 @@ Current user's request: ${currentInput}`;
1263
1263
  }
1264
1264
  // Filter and validate models before comparison
1265
1265
  const validModels = models.filter((m) => m && typeof m === "object" && typeof m.name === "string");
1266
- const targetModel = route.model || "llama3.2:latest";
1267
- const modelIsAvailable = validModels.some((m) => m.name === targetModel);
1268
- if (!modelIsAvailable) {
1266
+ const targetModel = route.model;
1267
+ if (targetModel) {
1268
+ // Check if the specific routed model is available
1269
+ const modelIsAvailable = validModels.some((m) => m.name === targetModel);
1270
+ if (!modelIsAvailable) {
1271
+ logger.debug("Orchestration provider validation failed", {
1272
+ taskType: classification.type,
1273
+ routedProvider: route.provider,
1274
+ routedModel: route.model,
1275
+ reason: `Ollama model '${targetModel}' not found`,
1276
+ orchestrationTime: `${Date.now() - startTime}ms`,
1277
+ });
1278
+ // Fall back to first available model instead of abandoning orchestration
1279
+ if (validModels.length > 0) {
1280
+ route.model = validModels[0].name;
1281
+ }
1282
+ else {
1283
+ return {}; // No models at all — preserve existing fallback behavior
1284
+ }
1285
+ }
1286
+ }
1287
+ else if (validModels.length === 0) {
1288
+ // No model specified and none available
1269
1289
  logger.debug("Orchestration provider validation failed", {
1270
1290
  taskType: classification.type,
1271
1291
  routedProvider: route.provider,
1272
1292
  routedModel: route.model,
1273
- reason: `Ollama model '${route.model || "llama3.2:latest"}' not found`,
1293
+ reason: "No Ollama models available",
1274
1294
  orchestrationTime: `${Date.now() - startTime}ms`,
1275
1295
  });
1276
1296
  return {}; // Return empty object to preserve existing fallback behavior
1277
1297
  }
1298
+ else {
1299
+ // No model specified but models are available — use the first one
1300
+ route.model = validModels[0].name;
1301
+ }
1278
1302
  }
1279
1303
  catch (error) {
1280
1304
  logger.debug("Orchestration provider validation failed", {
@@ -1377,18 +1401,42 @@ Current user's request: ${currentInput}`;
1377
1401
  }
1378
1402
  // Filter and validate models before comparison
1379
1403
  const validModels = models.filter((m) => m && typeof m === "object" && typeof m.name === "string");
1380
- const targetModel = route.model || "llama3.2:latest";
1381
- const modelIsAvailable = validModels.some((m) => m.name === targetModel);
1382
- if (!modelIsAvailable) {
1404
+ const targetModel = route.model;
1405
+ if (targetModel) {
1406
+ // Check if the specific routed model is available
1407
+ const modelIsAvailable = validModels.some((m) => m.name === targetModel);
1408
+ if (!modelIsAvailable) {
1409
+ logger.debug("Stream orchestration provider validation failed", {
1410
+ taskType: classification.type,
1411
+ routedProvider: route.provider,
1412
+ routedModel: route.model,
1413
+ reason: `Ollama model '${targetModel}' not found`,
1414
+ orchestrationTime: `${Date.now() - startTime}ms`,
1415
+ });
1416
+ // Fall back to first available model instead of abandoning orchestration
1417
+ if (validModels.length > 0) {
1418
+ route.model = validModels[0].name;
1419
+ }
1420
+ else {
1421
+ return {}; // No models at all — preserve existing fallback behavior
1422
+ }
1423
+ }
1424
+ }
1425
+ else if (validModels.length === 0) {
1426
+ // No model specified and none available
1383
1427
  logger.debug("Stream orchestration provider validation failed", {
1384
1428
  taskType: classification.type,
1385
1429
  routedProvider: route.provider,
1386
1430
  routedModel: route.model,
1387
- reason: `Ollama model '${route.model || "llama3.2:latest"}' not found`,
1431
+ reason: "No Ollama models available",
1388
1432
  orchestrationTime: `${Date.now() - startTime}ms`,
1389
1433
  });
1390
1434
  return {}; // Return empty object to preserve existing fallback behavior
1391
1435
  }
1436
+ else {
1437
+ // No model specified but models are available — use the first one
1438
+ route.model = validModels[0].name;
1439
+ }
1392
1440
  }
1393
1441
  catch (error) {
1394
1442
  logger.debug("Stream orchestration provider validation failed", {
@@ -1693,7 +1741,11 @@ Current user's request: ${currentInput}`;
1693
1741
  span.spanId = traceCtx.parentSpanId;
1694
1742
  span.parentSpanId = undefined;
1695
1743
  }
1696
- span = SpanSerializer.endSpan(span, SpanStatus.OK);
1744
+ // Mark failed generations with ERROR status so metrics count them correctly
1745
+ const spanStatus = data.success === false || data.error
1746
+ ? SpanStatus.ERROR
1747
+ : SpanStatus.OK;
1748
+ span = SpanSerializer.endSpan(span, spanStatus, data.error ? String(data.error) : undefined);
1697
1749
  span.durationMs = responseTime;
1698
1750
  if (usage) {
1699
1751
  span = SpanSerializer.enrichWithTokenUsage(span, {
@@ -2261,19 +2313,14 @@ Current user's request: ${currentInput}`;
2261
2313
  evaluation: textResult.evaluation
2262
2314
  ? {
2263
2315
  ...textResult.evaluation,
2264
- isOffTopic: textResult.evaluation
2265
- .isOffTopic ?? false,
2266
- alertSeverity: textResult.evaluation
2267
- .alertSeverity ?? "none",
2268
- reasoning: textResult.evaluation
2269
- .reasoning ?? "No evaluation provided",
2270
- evaluationModel: textResult.evaluation
2271
- .evaluationModel ?? "unknown",
2272
- evaluationTime: textResult.evaluation
2273
- .evaluationTime ?? Date.now(),
2274
- // Include evaluationDomain from original options
2275
- evaluationDomain: textResult.evaluation
2276
- .evaluationDomain ??
2316
+ isOffTopic: textResult.evaluation.isOffTopic ?? false,
2317
+ alertSeverity: textResult.evaluation.alertSeverity ??
2318
+ "none",
2319
+ reasoning: textResult.evaluation.reasoning ??
2320
+ "No evaluation provided",
2321
+ evaluationModel: textResult.evaluation.evaluationModel ?? "unknown",
2322
+ evaluationTime: textResult.evaluation.evaluationTime ?? Date.now(),
2323
+ evaluationDomain: textResult.evaluation.evaluationDomain ??
2277
2324
  textOptions.evaluationDomain ??
2278
2325
  factoryResult.domainType,
2279
2326
  }
@@ -2304,6 +2351,27 @@ Current user's request: ${currentInput}`;
2304
2351
  code: SpanStatusCode.ERROR,
2305
2352
  message: error instanceof Error ? error.message : String(error),
2306
2353
  });
2354
+ // Emit generation:end on error so metrics listeners still record the failure.
2355
+ // Note: variables declared inside try blocks are not accessible in error
2356
+ // handlers, so we extract what we can from the original input.
2357
+ const errProvider = typeof optionsOrPrompt === "object"
2358
+ ? optionsOrPrompt.provider || "unknown"
2359
+ : "unknown";
2360
+ const errModel = typeof optionsOrPrompt === "object"
2361
+ ? optionsOrPrompt.model || "unknown"
2362
+ : "unknown";
2363
+ try {
2364
+ this.emitter.emit("generation:end", {
2365
+ provider: errProvider,
2366
+ model: errModel,
2367
+ responseTime: 0,
2368
+ error: error instanceof Error ? error.message : String(error),
2369
+ success: false,
2370
+ });
2371
+ }
2372
+ catch (emitError) {
2373
+ void emitError; // non-blocking — error event emission is best-effort
2374
+ }
2307
2375
  throw error;
2308
2376
  }
2309
2377
  finally {
@@ -4363,7 +4431,7 @@ Current user's request: ${currentInput}`;
4363
4431
  // Accept audio when frames are present; sampleRateHz is optional (defaults applied later)
4364
4432
  const hasAudio = !!(options?.input?.audio &&
4365
4433
  options.input.audio.frames &&
4366
- typeof options.input.audio.frames[Symbol.asyncIterator] !== "undefined");
4434
+ typeof options.input.audio.frames[Symbol.asyncIterator] === "function");
4367
4435
  if (!hasText && !hasAudio) {
4368
4436
  throw new Error("Stream options must include either input.text or input.audio");
4369
4437
  }
@@ -5085,8 +5153,7 @@ Current user's request: ${currentInput}`;
5085
5153
  }
5086
5154
  // Check if the memory manager is Redis-backed (has updateAgenticLoopReport method)
5087
5155
  if (!("updateAgenticLoopReport" in this.conversationMemory) ||
5088
- typeof this.conversationMemory
5089
- .updateAgenticLoopReport !== "function") {
5156
+ typeof this.conversationMemory.updateAgenticLoopReport !== "function") {
5090
5157
  throw new ConversationMemoryError("updateAgenticLoopReport is only supported with Redis conversation memory.", "CONFIG_ERROR");
5091
5158
  }
5092
5159
  await withTimeout(this
@@ -5787,7 +5854,6 @@ Current user's request: ${currentInput}`;
5787
5854
  }
5788
5855
  const responseData = await response.json();
5789
5856
  const models = responseData?.models;
5790
- const defaultOllamaModel = "llama3.2:latest";
5791
5857
  // Runtime-safe guard: ensure models is an array with valid objects
5792
5858
  if (!Array.isArray(models)) {
5793
5859
  logger.warn("Ollama API returned invalid models format in testProvider", {
@@ -5798,15 +5864,14 @@ Current user's request: ${currentInput}`;
5798
5864
  }
5799
5865
  // Filter and validate models before comparison
5800
5866
  const validModels = models.filter((m) => m && typeof m === "object" && typeof m.name === "string");
5801
- const modelIsAvailable = validModels.some((m) => m.name === defaultOllamaModel);
5802
- if (modelIsAvailable) {
5867
+ if (validModels.length > 0) {
5803
5868
  return {
5804
5869
  provider: providerName,
5805
5870
  status: "working",
5806
5871
  configured: true,
5807
5872
  authenticated: true,
5808
5873
  responseTime: Date.now() - startTime,
5809
- model: defaultOllamaModel,
5874
+ model: validModels[0].name,
5810
5875
  };
5811
5876
  }
5812
5877
  else {
@@ -5815,7 +5880,7 @@ Current user's request: ${currentInput}`;
5815
5880
  status: "failed",
5816
5881
  configured: true,
5817
5882
  authenticated: false,
5818
- error: `Ollama service running but model '${defaultOllamaModel}' not found`,
5883
+ error: "Ollama service running but no models installed",
5819
5884
  responseTime: Date.now() - startTime,
5820
5885
  };
5821
5886
  }
@@ -118,7 +118,9 @@ export class LangfuseExporter extends BaseExporter {
118
118
  name: span.name,
119
119
  userId: span.attributes["user.id"],
120
120
  sessionId: span.attributes["session.id"],
121
- metadata: span.attributes,
121
+ // Only pick safe, non-PII attributes for metadata — intentionally excludes
122
+ // input, output, error.stack, and other user content to match Braintrust exporter
123
+ metadata: filterSafeMetadata(span.attributes),
122
124
  release: this.release,
123
125
  tags: this.extractTags(span),
124
126
  };
@@ -197,4 +199,6 @@ export class LangfuseExporter extends BaseExporter {
197
199
  return tags;
198
200
  }
199
201
  }
202
+ // Safe metadata filtering imported from shared module to avoid duplication
203
+ import { filterSafeMetadata } from "../utils/safeMetadata.js";
200
204
  //# sourceMappingURL=langfuseExporter.js.map
@@ -35,8 +35,12 @@ export class MetricsAggregator {
35
35
  recordSpan(span) {
36
36
  // Enforce maximum spans limit
37
37
  if (this.spans.length >= this.config.maxSpansRetained) {
38
- this.spans.shift(); // Remove oldest span
39
- // Note: We keep aggregated metrics, only raw spans are trimmed
38
+ const evicted = this.spans.shift(); // Remove oldest span
39
+ // Only trim latencyValues when the evicted span had a duration recorded
40
+ if (evicted?.durationMs !== undefined) {
41
+ this.latencyValues.shift();
42
+ }
43
+ // Note: We keep aggregated metrics, only raw spans and latency values are trimmed
40
44
  }
41
45
  this.spans.push(span);
42
46
  // Update timestamps
@@ -66,6 +66,8 @@ export class RedactionProcessor {
66
66
  "credentials",
67
67
  "private_key",
68
68
  "privateKey",
69
+ "stack",
70
+ "error.stack",
69
71
  ]);
70
72
  this.redactedValue = config?.redactedValue ?? "[REDACTED]";
71
73
  }
@@ -228,11 +230,25 @@ export class BatchProcessor {
228
230
  this.flush();
229
231
  }, this.flushIntervalMs);
230
232
  }
233
+ // Note: flush() is intentionally synchronous. The onBatchReady callback is
234
+ // typed as `(spans: SpanData[]) => void` — callers must not pass async
235
+ // exporters. If async export is needed, the callback should handle its own
236
+ // error reporting (e.g. fire-and-forget with promise error handlers).
231
237
  flush() {
232
238
  if (this.batch.length > 0 && this.onBatchReady) {
233
239
  const spans = [...this.batch];
234
- this.batch = [];
235
- this.onBatchReady(spans);
240
+ try {
241
+ this.onBatchReady(spans);
242
+ this.batch = [];
243
+ }
244
+ catch (flushError) {
245
+ // Keep spans for next flush attempt, but cap backlog growth
246
+ void flushError; // acknowledged — error is expected during exporter outages
247
+ const maxBacklog = this.batchSize * 20;
248
+ if (this.batch.length > maxBacklog) {
249
+ this.batch = this.batch.slice(this.batch.length - maxBacklog);
250
+ }
251
+ }
236
252
  }
237
253
  }
238
254
  async shutdown() {
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Safe metadata filtering for observability exporters.
3
+ *
4
+ * Only these attribute keys are forwarded to third-party backends as trace
5
+ * metadata. User prompts (input), LLM responses (output), error stacks, and
6
+ * any other potentially sensitive data are excluded to prevent PII leaks.
7
+ */
8
+ import type { SpanAttributes } from "../types/spanTypes.js";
9
+ export declare const SAFE_METADATA_KEYS: Set<string>;
10
+ export declare function filterSafeMetadata(attributes: SpanAttributes): Record<string, unknown>;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Safe metadata filtering for observability exporters.
3
+ *
4
+ * Only these attribute keys are forwarded to third-party backends as trace
5
+ * metadata. User prompts (input), LLM responses (output), error stacks, and
6
+ * any other potentially sensitive data are excluded to prevent PII leaks.
7
+ */
8
+ // Only ai.* keys are forwarded as metadata. Stream metrics (chunk_count,
9
+ // content_length) should be accessed via span attributes directly, not via
10
+ // metadata sent to third-party backends.
11
+ export const SAFE_METADATA_KEYS = new Set([
12
+ "ai.provider",
13
+ "ai.model",
14
+ "ai.temperature",
15
+ "ai.max_tokens",
16
+ ]);
17
+ export function filterSafeMetadata(attributes) {
18
+ const filtered = {};
19
+ for (const key of SAFE_METADATA_KEYS) {
20
+ if (attributes[key] !== undefined) {
21
+ filtered[key] = attributes[key];
22
+ }
23
+ }
24
+ return filtered;
25
+ }
26
+ //# sourceMappingURL=safeMetadata.js.map
@@ -104,7 +104,9 @@ export class SpanSerializer {
104
104
  name: span.name,
105
105
  startTime: span.startTime,
106
106
  endTime: span.endTime,
107
- metadata: { ...span.attributes },
107
+ // Only pick safe, non-PII attributes for metadata — intentionally excludes
108
+ // input, output, error.stack, and other user content for PII safety
109
+ metadata: filterSafeMetadata(span.attributes),
108
110
  level: span.status === SpanStatus.ERROR ? "ERROR" : "DEFAULT",
109
111
  statusMessage: span.statusMessage,
110
112
  input: span.attributes["input"],
@@ -284,4 +286,6 @@ export class SpanSerializer {
284
286
  });
285
287
  }
286
288
  }
289
+ // Safe metadata filtering imported from shared module to avoid duplication
290
+ import { filterSafeMetadata } from "./safeMetadata.js";
287
291
  //# sourceMappingURL=spanSerializer.js.map
@@ -1259,9 +1259,7 @@ export class GoogleVertexProvider extends BaseProvider {
1259
1259
  },
1260
1260
  {
1261
1261
  role: "model",
1262
- parts: [
1263
- { text: "Understood. I will follow these instructions." },
1264
- ],
1262
+ parts: [{ text: "OK" }],
1265
1263
  },
1266
1264
  ...currentContents,
1267
1265
  ];
@@ -1435,9 +1433,7 @@ export class GoogleVertexProvider extends BaseProvider {
1435
1433
  },
1436
1434
  {
1437
1435
  role: "model",
1438
- parts: [
1439
- { text: "Understood. I will follow these instructions." },
1440
- ],
1436
+ parts: [{ text: "OK" }],
1441
1437
  },
1442
1438
  ...currentContents,
1443
1439
  ];
package/dist/neurolink.js CHANGED
@@ -1263,18 +1263,42 @@ Current user's request: ${currentInput}`;
1263
1263
  }
1264
1264
  // Filter and validate models before comparison
1265
1265
  const validModels = models.filter((m) => m && typeof m === "object" && typeof m.name === "string");
1266
- const targetModel = route.model || "llama3.2:latest";
1267
- const modelIsAvailable = validModels.some((m) => m.name === targetModel);
1268
- if (!modelIsAvailable) {
1266
+ const targetModel = route.model;
1267
+ if (targetModel) {
1268
+ // Check if the specific routed model is available
1269
+ const modelIsAvailable = validModels.some((m) => m.name === targetModel);
1270
+ if (!modelIsAvailable) {
1271
+ logger.debug("Orchestration provider validation failed", {
1272
+ taskType: classification.type,
1273
+ routedProvider: route.provider,
1274
+ routedModel: route.model,
1275
+ reason: `Ollama model '${targetModel}' not found`,
1276
+ orchestrationTime: `${Date.now() - startTime}ms`,
1277
+ });
1278
+ // Fall back to first available model instead of abandoning orchestration
1279
+ if (validModels.length > 0) {
1280
+ route.model = validModels[0].name;
1281
+ }
1282
+ else {
1283
+ return {}; // No models at all — preserve existing fallback behavior
1284
+ }
1285
+ }
1286
+ }
1287
+ else if (validModels.length === 0) {
1288
+ // No model specified and none available
1269
1289
  logger.debug("Orchestration provider validation failed", {
1270
1290
  taskType: classification.type,
1271
1291
  routedProvider: route.provider,
1272
1292
  routedModel: route.model,
1273
- reason: `Ollama model '${route.model || "llama3.2:latest"}' not found`,
1293
+ reason: "No Ollama models available",
1274
1294
  orchestrationTime: `${Date.now() - startTime}ms`,
1275
1295
  });
1276
1296
  return {}; // Return empty object to preserve existing fallback behavior
1277
1297
  }
1298
+ else {
1299
+ // No model specified but models are available — use the first one
1300
+ route.model = validModels[0].name;
1301
+ }
1278
1302
  }
1279
1303
  catch (error) {
1280
1304
  logger.debug("Orchestration provider validation failed", {
@@ -1377,18 +1401,42 @@ Current user's request: ${currentInput}`;
1377
1401
  }
1378
1402
  // Filter and validate models before comparison
1379
1403
  const validModels = models.filter((m) => m && typeof m === "object" && typeof m.name === "string");
1380
- const targetModel = route.model || "llama3.2:latest";
1381
- const modelIsAvailable = validModels.some((m) => m.name === targetModel);
1382
- if (!modelIsAvailable) {
1404
+ const targetModel = route.model;
1405
+ if (targetModel) {
1406
+ // Check if the specific routed model is available
1407
+ const modelIsAvailable = validModels.some((m) => m.name === targetModel);
1408
+ if (!modelIsAvailable) {
1409
+ logger.debug("Stream orchestration provider validation failed", {
1410
+ taskType: classification.type,
1411
+ routedProvider: route.provider,
1412
+ routedModel: route.model,
1413
+ reason: `Ollama model '${targetModel}' not found`,
1414
+ orchestrationTime: `${Date.now() - startTime}ms`,
1415
+ });
1416
+ // Fall back to first available model instead of abandoning orchestration
1417
+ if (validModels.length > 0) {
1418
+ route.model = validModels[0].name;
1419
+ }
1420
+ else {
1421
+ return {}; // No models at all — preserve existing fallback behavior
1422
+ }
1423
+ }
1424
+ }
1425
+ else if (validModels.length === 0) {
1426
+ // No model specified and none available
1383
1427
  logger.debug("Stream orchestration provider validation failed", {
1384
1428
  taskType: classification.type,
1385
1429
  routedProvider: route.provider,
1386
1430
  routedModel: route.model,
1387
- reason: `Ollama model '${route.model || "llama3.2:latest"}' not found`,
1431
+ reason: "No Ollama models available",
1388
1432
  orchestrationTime: `${Date.now() - startTime}ms`,
1389
1433
  });
1390
1434
  return {}; // Return empty object to preserve existing fallback behavior
1391
1435
  }
1436
+ else {
1437
+ // No model specified but models are available — use the first one
1438
+ route.model = validModels[0].name;
1439
+ }
1392
1440
  }
1393
1441
  catch (error) {
1394
1442
  logger.debug("Stream orchestration provider validation failed", {
@@ -1693,7 +1741,11 @@ Current user's request: ${currentInput}`;
1693
1741
  span.spanId = traceCtx.parentSpanId;
1694
1742
  span.parentSpanId = undefined;
1695
1743
  }
1696
- span = SpanSerializer.endSpan(span, SpanStatus.OK);
1744
+ // Mark failed generations with ERROR status so metrics count them correctly
1745
+ const spanStatus = data.success === false || data.error
1746
+ ? SpanStatus.ERROR
1747
+ : SpanStatus.OK;
1748
+ span = SpanSerializer.endSpan(span, spanStatus, data.error ? String(data.error) : undefined);
1697
1749
  span.durationMs = responseTime;
1698
1750
  if (usage) {
1699
1751
  span = SpanSerializer.enrichWithTokenUsage(span, {
@@ -2261,19 +2313,14 @@ Current user's request: ${currentInput}`;
2261
2313
  evaluation: textResult.evaluation
2262
2314
  ? {
2263
2315
  ...textResult.evaluation,
2264
- isOffTopic: textResult.evaluation
2265
- .isOffTopic ?? false,
2266
- alertSeverity: textResult.evaluation
2267
- .alertSeverity ?? "none",
2268
- reasoning: textResult.evaluation
2269
- .reasoning ?? "No evaluation provided",
2270
- evaluationModel: textResult.evaluation
2271
- .evaluationModel ?? "unknown",
2272
- evaluationTime: textResult.evaluation
2273
- .evaluationTime ?? Date.now(),
2274
- // Include evaluationDomain from original options
2275
- evaluationDomain: textResult.evaluation
2276
- .evaluationDomain ??
2316
+ isOffTopic: textResult.evaluation.isOffTopic ?? false,
2317
+ alertSeverity: textResult.evaluation.alertSeverity ??
2318
+ "none",
2319
+ reasoning: textResult.evaluation.reasoning ??
2320
+ "No evaluation provided",
2321
+ evaluationModel: textResult.evaluation.evaluationModel ?? "unknown",
2322
+ evaluationTime: textResult.evaluation.evaluationTime ?? Date.now(),
2323
+ evaluationDomain: textResult.evaluation.evaluationDomain ??
2277
2324
  textOptions.evaluationDomain ??
2278
2325
  factoryResult.domainType,
2279
2326
  }
@@ -2304,6 +2351,27 @@ Current user's request: ${currentInput}`;
2304
2351
  code: SpanStatusCode.ERROR,
2305
2352
  message: error instanceof Error ? error.message : String(error),
2306
2353
  });
2354
+ // Emit generation:end on error so metrics listeners still record the failure.
2355
+ // Note: variables declared inside try blocks are not accessible in error
2356
+ // handlers, so we extract what we can from the original input.
2357
+ const errProvider = typeof optionsOrPrompt === "object"
2358
+ ? optionsOrPrompt.provider || "unknown"
2359
+ : "unknown";
2360
+ const errModel = typeof optionsOrPrompt === "object"
2361
+ ? optionsOrPrompt.model || "unknown"
2362
+ : "unknown";
2363
+ try {
2364
+ this.emitter.emit("generation:end", {
2365
+ provider: errProvider,
2366
+ model: errModel,
2367
+ responseTime: 0,
2368
+ error: error instanceof Error ? error.message : String(error),
2369
+ success: false,
2370
+ });
2371
+ }
2372
+ catch (emitError) {
2373
+ void emitError; // non-blocking — error event emission is best-effort
2374
+ }
2307
2375
  throw error;
2308
2376
  }
2309
2377
  finally {
@@ -4363,7 +4431,7 @@ Current user's request: ${currentInput}`;
4363
4431
  // Accept audio when frames are present; sampleRateHz is optional (defaults applied later)
4364
4432
  const hasAudio = !!(options?.input?.audio &&
4365
4433
  options.input.audio.frames &&
4366
- typeof options.input.audio.frames[Symbol.asyncIterator] !== "undefined");
4434
+ typeof options.input.audio.frames[Symbol.asyncIterator] === "function");
4367
4435
  if (!hasText && !hasAudio) {
4368
4436
  throw new Error("Stream options must include either input.text or input.audio");
4369
4437
  }
@@ -5085,8 +5153,7 @@ Current user's request: ${currentInput}`;
5085
5153
  }
5086
5154
  // Check if the memory manager is Redis-backed (has updateAgenticLoopReport method)
5087
5155
  if (!("updateAgenticLoopReport" in this.conversationMemory) ||
5088
- typeof this.conversationMemory
5089
- .updateAgenticLoopReport !== "function") {
5156
+ typeof this.conversationMemory.updateAgenticLoopReport !== "function") {
5090
5157
  throw new ConversationMemoryError("updateAgenticLoopReport is only supported with Redis conversation memory.", "CONFIG_ERROR");
5091
5158
  }
5092
5159
  await withTimeout(this
@@ -5787,7 +5854,6 @@ Current user's request: ${currentInput}`;
5787
5854
  }
5788
5855
  const responseData = await response.json();
5789
5856
  const models = responseData?.models;
5790
- const defaultOllamaModel = "llama3.2:latest";
5791
5857
  // Runtime-safe guard: ensure models is an array with valid objects
5792
5858
  if (!Array.isArray(models)) {
5793
5859
  logger.warn("Ollama API returned invalid models format in testProvider", {
@@ -5798,15 +5864,14 @@ Current user's request: ${currentInput}`;
5798
5864
  }
5799
5865
  // Filter and validate models before comparison
5800
5866
  const validModels = models.filter((m) => m && typeof m === "object" && typeof m.name === "string");
5801
- const modelIsAvailable = validModels.some((m) => m.name === defaultOllamaModel);
5802
- if (modelIsAvailable) {
5867
+ if (validModels.length > 0) {
5803
5868
  return {
5804
5869
  provider: providerName,
5805
5870
  status: "working",
5806
5871
  configured: true,
5807
5872
  authenticated: true,
5808
5873
  responseTime: Date.now() - startTime,
5809
- model: defaultOllamaModel,
5874
+ model: validModels[0].name,
5810
5875
  };
5811
5876
  }
5812
5877
  else {
@@ -5815,7 +5880,7 @@ Current user's request: ${currentInput}`;
5815
5880
  status: "failed",
5816
5881
  configured: true,
5817
5882
  authenticated: false,
5818
- error: `Ollama service running but model '${defaultOllamaModel}' not found`,
5883
+ error: "Ollama service running but no models installed",
5819
5884
  responseTime: Date.now() - startTime,
5820
5885
  };
5821
5886
  }
@@ -118,7 +118,9 @@ export class LangfuseExporter extends BaseExporter {
118
118
  name: span.name,
119
119
  userId: span.attributes["user.id"],
120
120
  sessionId: span.attributes["session.id"],
121
- metadata: span.attributes,
121
+ // Only pick safe, non-PII attributes for metadata — intentionally excludes
122
+ // input, output, error.stack, and other user content to match Braintrust exporter
123
+ metadata: filterSafeMetadata(span.attributes),
122
124
  release: this.release,
123
125
  tags: this.extractTags(span),
124
126
  };
@@ -197,3 +199,5 @@ export class LangfuseExporter extends BaseExporter {
197
199
  return tags;
198
200
  }
199
201
  }
202
+ // Safe metadata filtering imported from shared module to avoid duplication
203
+ import { filterSafeMetadata } from "../utils/safeMetadata.js";
@@ -35,8 +35,12 @@ export class MetricsAggregator {
35
35
  recordSpan(span) {
36
36
  // Enforce maximum spans limit
37
37
  if (this.spans.length >= this.config.maxSpansRetained) {
38
- this.spans.shift(); // Remove oldest span
39
- // Note: We keep aggregated metrics, only raw spans are trimmed
38
+ const evicted = this.spans.shift(); // Remove oldest span
39
+ // Only trim latencyValues when the evicted span had a duration recorded
40
+ if (evicted?.durationMs !== undefined) {
41
+ this.latencyValues.shift();
42
+ }
43
+ // Note: We keep aggregated metrics, only raw spans and latency values are trimmed
40
44
  }
41
45
  this.spans.push(span);
42
46
  // Update timestamps
@@ -66,6 +66,8 @@ export class RedactionProcessor {
66
66
  "credentials",
67
67
  "private_key",
68
68
  "privateKey",
69
+ "stack",
70
+ "error.stack",
69
71
  ]);
70
72
  this.redactedValue = config?.redactedValue ?? "[REDACTED]";
71
73
  }
@@ -228,11 +230,25 @@ export class BatchProcessor {
228
230
  this.flush();
229
231
  }, this.flushIntervalMs);
230
232
  }
233
+ // Note: flush() is intentionally synchronous. The onBatchReady callback is
234
+ // typed as `(spans: SpanData[]) => void` — callers must not pass async
235
+ // exporters. If async export is needed, the callback should handle its own
236
+ // error reporting (e.g. fire-and-forget with promise error handlers).
231
237
  flush() {
232
238
  if (this.batch.length > 0 && this.onBatchReady) {
233
239
  const spans = [...this.batch];
234
- this.batch = [];
235
- this.onBatchReady(spans);
240
+ try {
241
+ this.onBatchReady(spans);
242
+ this.batch = [];
243
+ }
244
+ catch (flushError) {
245
+ // Keep spans for next flush attempt, but cap backlog growth
246
+ void flushError; // acknowledged — error is expected during exporter outages
247
+ const maxBacklog = this.batchSize * 20;
248
+ if (this.batch.length > maxBacklog) {
249
+ this.batch = this.batch.slice(this.batch.length - maxBacklog);
250
+ }
251
+ }
236
252
  }
237
253
  }
238
254
  async shutdown() {
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Safe metadata filtering for observability exporters.
3
+ *
4
+ * Only these attribute keys are forwarded to third-party backends as trace
5
+ * metadata. User prompts (input), LLM responses (output), error stacks, and
6
+ * any other potentially sensitive data are excluded to prevent PII leaks.
7
+ */
8
+ import type { SpanAttributes } from "../types/spanTypes.js";
9
+ export declare const SAFE_METADATA_KEYS: Set<string>;
10
+ export declare function filterSafeMetadata(attributes: SpanAttributes): Record<string, unknown>;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Safe metadata filtering for observability exporters.
3
+ *
4
+ * Only these attribute keys are forwarded to third-party backends as trace
5
+ * metadata. User prompts (input), LLM responses (output), error stacks, and
6
+ * any other potentially sensitive data are excluded to prevent PII leaks.
7
+ */
8
+ // Only ai.* keys are forwarded as metadata. Stream metrics (chunk_count,
9
+ // content_length) should be accessed via span attributes directly, not via
10
+ // metadata sent to third-party backends.
11
+ export const SAFE_METADATA_KEYS = new Set([
12
+ "ai.provider",
13
+ "ai.model",
14
+ "ai.temperature",
15
+ "ai.max_tokens",
16
+ ]);
17
+ export function filterSafeMetadata(attributes) {
18
+ const filtered = {};
19
+ for (const key of SAFE_METADATA_KEYS) {
20
+ if (attributes[key] !== undefined) {
21
+ filtered[key] = attributes[key];
22
+ }
23
+ }
24
+ return filtered;
25
+ }
@@ -104,7 +104,9 @@ export class SpanSerializer {
104
104
  name: span.name,
105
105
  startTime: span.startTime,
106
106
  endTime: span.endTime,
107
- metadata: { ...span.attributes },
107
+ // Only pick safe, non-PII attributes for metadata — intentionally excludes
108
+ // input, output, error.stack, and other user content for PII safety
109
+ metadata: filterSafeMetadata(span.attributes),
108
110
  level: span.status === SpanStatus.ERROR ? "ERROR" : "DEFAULT",
109
111
  statusMessage: span.statusMessage,
110
112
  input: span.attributes["input"],
@@ -284,3 +286,5 @@ export class SpanSerializer {
284
286
  });
285
287
  }
286
288
  }
289
+ // Safe metadata filtering imported from shared module to avoid duplication
290
+ import { filterSafeMetadata } from "./safeMetadata.js";
@@ -1259,9 +1259,7 @@ export class GoogleVertexProvider extends BaseProvider {
1259
1259
  },
1260
1260
  {
1261
1261
  role: "model",
1262
- parts: [
1263
- { text: "Understood. I will follow these instructions." },
1264
- ],
1262
+ parts: [{ text: "OK" }],
1265
1263
  },
1266
1264
  ...currentContents,
1267
1265
  ];
@@ -1435,9 +1433,7 @@ export class GoogleVertexProvider extends BaseProvider {
1435
1433
  },
1436
1434
  {
1437
1435
  role: "model",
1438
- parts: [
1439
- { text: "Understood. I will follow these instructions." },
1440
- ],
1436
+ parts: [{ text: "OK" }],
1441
1437
  },
1442
1438
  ...currentContents,
1443
1439
  ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/neurolink",
3
- "version": "9.25.0",
3
+ "version": "9.25.2",
4
4
  "description": "Universal AI Development Platform with working MCP integration, multi-provider support, and professional CLI. Built-in tools operational, 58+ external MCP servers discoverable. Connect to filesystem, GitHub, database operations, and more. Build, test, and deploy AI applications with 13 providers: OpenAI, Anthropic, Google AI, AWS Bedrock, Azure, Hugging Face, Ollama, and Mistral AI.",
5
5
  "author": {
6
6
  "name": "Juspay Technologies",