@juspay/neurolink 9.42.0 → 9.43.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.
Files changed (116) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/auth/anthropicOAuth.js +12 -0
  3. package/dist/browser/neurolink.min.js +335 -334
  4. package/dist/cli/commands/mcp.d.ts +6 -0
  5. package/dist/cli/commands/mcp.js +200 -184
  6. package/dist/cli/commands/proxy.js +560 -518
  7. package/dist/core/baseProvider.d.ts +6 -1
  8. package/dist/core/baseProvider.js +219 -232
  9. package/dist/core/factory.d.ts +3 -0
  10. package/dist/core/factory.js +140 -190
  11. package/dist/core/modules/ToolsManager.d.ts +1 -0
  12. package/dist/core/modules/ToolsManager.js +40 -42
  13. package/dist/core/toolEvents.d.ts +3 -0
  14. package/dist/core/toolEvents.js +7 -0
  15. package/dist/evaluation/pipeline/evaluationPipeline.js +5 -2
  16. package/dist/evaluation/scorers/scorerRegistry.d.ts +3 -0
  17. package/dist/evaluation/scorers/scorerRegistry.js +356 -284
  18. package/dist/lib/auth/anthropicOAuth.js +12 -0
  19. package/dist/lib/core/baseProvider.d.ts +6 -1
  20. package/dist/lib/core/baseProvider.js +219 -232
  21. package/dist/lib/core/factory.d.ts +3 -0
  22. package/dist/lib/core/factory.js +140 -190
  23. package/dist/lib/core/modules/ToolsManager.d.ts +1 -0
  24. package/dist/lib/core/modules/ToolsManager.js +40 -42
  25. package/dist/lib/core/toolEvents.d.ts +3 -0
  26. package/dist/lib/core/toolEvents.js +8 -0
  27. package/dist/lib/evaluation/pipeline/evaluationPipeline.js +5 -2
  28. package/dist/lib/evaluation/scorers/scorerRegistry.d.ts +3 -0
  29. package/dist/lib/evaluation/scorers/scorerRegistry.js +356 -284
  30. package/dist/lib/mcp/toolRegistry.d.ts +2 -0
  31. package/dist/lib/mcp/toolRegistry.js +32 -31
  32. package/dist/lib/neurolink.d.ts +38 -0
  33. package/dist/lib/neurolink.js +1890 -1707
  34. package/dist/lib/providers/googleAiStudio.js +0 -5
  35. package/dist/lib/providers/googleNativeGemini3.d.ts +4 -0
  36. package/dist/lib/providers/googleNativeGemini3.js +39 -1
  37. package/dist/lib/providers/googleVertex.d.ts +10 -0
  38. package/dist/lib/providers/googleVertex.js +445 -445
  39. package/dist/lib/providers/litellm.d.ts +1 -0
  40. package/dist/lib/providers/litellm.js +73 -64
  41. package/dist/lib/providers/ollama.js +17 -4
  42. package/dist/lib/providers/openAI.d.ts +2 -0
  43. package/dist/lib/providers/openAI.js +139 -140
  44. package/dist/lib/proxy/claudeFormat.js +14 -5
  45. package/dist/lib/proxy/oauthFetch.js +298 -318
  46. package/dist/lib/proxy/proxyConfig.js +3 -1
  47. package/dist/lib/proxy/proxyFetch.js +250 -222
  48. package/dist/lib/proxy/proxyHealth.d.ts +17 -0
  49. package/dist/lib/proxy/proxyHealth.js +55 -0
  50. package/dist/lib/proxy/requestLogger.js +140 -48
  51. package/dist/lib/proxy/routingPolicy.d.ts +33 -0
  52. package/dist/lib/proxy/routingPolicy.js +255 -0
  53. package/dist/lib/proxy/snapshotPersistence.d.ts +2 -0
  54. package/dist/lib/proxy/snapshotPersistence.js +41 -0
  55. package/dist/lib/proxy/sseInterceptor.js +36 -11
  56. package/dist/lib/server/routes/claudeProxyRoutes.d.ts +2 -1
  57. package/dist/lib/server/routes/claudeProxyRoutes.js +2916 -2377
  58. package/dist/lib/services/server/ai/observability/instrumentation.js +194 -218
  59. package/dist/lib/tasks/backends/bullmqBackend.js +24 -18
  60. package/dist/lib/tasks/store/redisTaskStore.js +42 -17
  61. package/dist/lib/tasks/taskManager.d.ts +2 -0
  62. package/dist/lib/tasks/taskManager.js +100 -5
  63. package/dist/lib/telemetry/telemetryService.js +9 -5
  64. package/dist/lib/types/cli.d.ts +4 -0
  65. package/dist/lib/types/proxyTypes.d.ts +211 -1
  66. package/dist/lib/types/tools.d.ts +18 -0
  67. package/dist/lib/utils/providerHealth.d.ts +1 -0
  68. package/dist/lib/utils/providerHealth.js +46 -31
  69. package/dist/lib/utils/providerUtils.js +11 -22
  70. package/dist/lib/utils/schemaConversion.d.ts +1 -0
  71. package/dist/lib/utils/schemaConversion.js +3 -0
  72. package/dist/mcp/toolRegistry.d.ts +2 -0
  73. package/dist/mcp/toolRegistry.js +32 -31
  74. package/dist/neurolink.d.ts +38 -0
  75. package/dist/neurolink.js +1890 -1707
  76. package/dist/providers/googleAiStudio.js +0 -5
  77. package/dist/providers/googleNativeGemini3.d.ts +4 -0
  78. package/dist/providers/googleNativeGemini3.js +39 -1
  79. package/dist/providers/googleVertex.d.ts +10 -0
  80. package/dist/providers/googleVertex.js +445 -445
  81. package/dist/providers/litellm.d.ts +1 -0
  82. package/dist/providers/litellm.js +73 -64
  83. package/dist/providers/ollama.js +17 -4
  84. package/dist/providers/openAI.d.ts +2 -0
  85. package/dist/providers/openAI.js +139 -140
  86. package/dist/proxy/claudeFormat.js +14 -5
  87. package/dist/proxy/oauthFetch.js +298 -318
  88. package/dist/proxy/proxyConfig.js +3 -1
  89. package/dist/proxy/proxyFetch.js +250 -222
  90. package/dist/proxy/proxyHealth.d.ts +17 -0
  91. package/dist/proxy/proxyHealth.js +54 -0
  92. package/dist/proxy/requestLogger.js +140 -48
  93. package/dist/proxy/routingPolicy.d.ts +33 -0
  94. package/dist/proxy/routingPolicy.js +254 -0
  95. package/dist/proxy/snapshotPersistence.d.ts +2 -0
  96. package/dist/proxy/snapshotPersistence.js +40 -0
  97. package/dist/proxy/sseInterceptor.js +36 -11
  98. package/dist/server/routes/claudeProxyRoutes.d.ts +2 -1
  99. package/dist/server/routes/claudeProxyRoutes.js +2916 -2377
  100. package/dist/services/server/ai/observability/instrumentation.js +194 -218
  101. package/dist/tasks/backends/bullmqBackend.js +24 -18
  102. package/dist/tasks/store/redisTaskStore.js +42 -17
  103. package/dist/tasks/taskManager.d.ts +2 -0
  104. package/dist/tasks/taskManager.js +100 -5
  105. package/dist/telemetry/telemetryService.js +9 -5
  106. package/dist/types/cli.d.ts +4 -0
  107. package/dist/types/proxyTypes.d.ts +211 -1
  108. package/dist/types/tools.d.ts +18 -0
  109. package/dist/utils/providerHealth.d.ts +1 -0
  110. package/dist/utils/providerHealth.js +46 -31
  111. package/dist/utils/providerUtils.js +12 -22
  112. package/dist/utils/schemaConversion.d.ts +1 -0
  113. package/dist/utils/schemaConversion.js +3 -0
  114. package/package.json +3 -2
  115. package/scripts/observability/check-proxy-telemetry.mjs +1 -1
  116. package/scripts/observability/manage-local-openobserve.sh +36 -5
@@ -158,7 +158,28 @@ export class TaskManager {
158
158
  await backend.schedule(task, (t) => this.onTaskTick(t));
159
159
  }
160
160
  catch (err) {
161
- await store.delete(task.id);
161
+ this.callbacks.delete(task.id);
162
+ try {
163
+ await store.delete(task.id);
164
+ }
165
+ catch (cleanupError) {
166
+ // Deletion failed — task remains persisted as active. Attempt to mark it
167
+ // failed so it reaches a terminal state and operators can identify it.
168
+ logger.error("[TaskManager] Failed to clean up task after schedule error — task may remain persisted as active", {
169
+ taskId: task.id,
170
+ scheduleError: String(err),
171
+ cleanupError: String(cleanupError),
172
+ });
173
+ try {
174
+ await store.update(task.id, { status: "failed" });
175
+ }
176
+ catch (terminalError) {
177
+ logger.error("[TaskManager] Failed to force task to terminal state — manual cleanup required", {
178
+ taskId: task.id,
179
+ error: String(terminalError),
180
+ });
181
+ }
182
+ }
162
183
  throw err;
163
184
  }
164
185
  this.emit("task:created", task);
@@ -209,6 +230,7 @@ export class TaskManager {
209
230
  taskUpdates[field] = updates[field];
210
231
  }
211
232
  }
233
+ const shouldClearHistory = updates.mode !== undefined && updates.mode !== "continuation";
212
234
  // Special-case: mode changes require sessionId handling
213
235
  if (updates.mode !== undefined) {
214
236
  if (updates.mode === "continuation" && !existing.sessionId) {
@@ -216,14 +238,39 @@ export class TaskManager {
216
238
  }
217
239
  else if (updates.mode !== "continuation") {
218
240
  taskUpdates.sessionId = undefined;
219
- await store.clearHistory(taskId);
220
241
  }
221
242
  }
222
243
  const updated = await store.update(taskId, taskUpdates);
223
244
  // Re-schedule if schedule changed and task is active
224
245
  if (updates.schedule && updated.status === "active") {
246
+ const attemptedSchedule = updated.schedule;
225
247
  await backend.cancel(taskId);
226
- await backend.schedule(updated, (t) => this.onTaskTick(t));
248
+ try {
249
+ await backend.schedule(updated, (t) => this.onTaskTick(t));
250
+ }
251
+ catch (error) {
252
+ await this.restoreScheduledTask(existing, "update schedule rollback");
253
+ await this.rollbackTaskUpdate(taskId, existing, error);
254
+ throw TaskError.create("SCHEDULE_FAILED", `Failed to update schedule for task ${taskId}`, {
255
+ cause: error instanceof Error ? error : undefined,
256
+ details: {
257
+ taskId,
258
+ previousSchedule: existing.schedule,
259
+ attemptedSchedule,
260
+ },
261
+ });
262
+ }
263
+ }
264
+ if (shouldClearHistory) {
265
+ try {
266
+ await store.clearHistory(taskId);
267
+ }
268
+ catch (error) {
269
+ logger.warn("[TaskManager] Failed to clear task history after mode update", {
270
+ taskId,
271
+ error: String(error),
272
+ });
273
+ }
227
274
  }
228
275
  return updated;
229
276
  }
@@ -248,7 +295,14 @@ export class TaskManager {
248
295
  throw TaskError.create("INVALID_TASK_STATUS", `Cannot pause task with status: ${task.status}`);
249
296
  }
250
297
  await backend.pause(taskId);
251
- const updated = await store.update(taskId, { status: "paused" });
298
+ let updated;
299
+ try {
300
+ updated = await store.update(taskId, { status: "paused" });
301
+ }
302
+ catch (error) {
303
+ await this.restoreScheduledTask(task, "pause rollback");
304
+ throw error;
305
+ }
252
306
  this.emit("task:paused", updated);
253
307
  return updated;
254
308
  }
@@ -264,7 +318,16 @@ export class TaskManager {
264
318
  throw TaskError.create("INVALID_TASK_STATUS", `Cannot resume task with status: ${task.status}`);
265
319
  }
266
320
  const updated = await store.update(taskId, { status: "active" });
267
- await backend.schedule(updated, (t) => this.onTaskTick(t));
321
+ try {
322
+ await backend.schedule(updated, (t) => this.onTaskTick(t));
323
+ }
324
+ catch (error) {
325
+ await this.rollbackTaskUpdate(taskId, task, error);
326
+ throw TaskError.create("SCHEDULE_FAILED", `Failed to resume task ${taskId}`, {
327
+ cause: error instanceof Error ? error : undefined,
328
+ details: { taskId, schedule: task.schedule },
329
+ });
330
+ }
268
331
  this.emit("task:resumed", updated);
269
332
  return updated;
270
333
  }
@@ -301,6 +364,38 @@ export class TaskManager {
301
364
  return this.backend.isHealthy();
302
365
  }
303
366
  // ── Internal ──────────────────────────────────────────
367
+ async restoreScheduledTask(task, reason) {
368
+ if (task.status !== "active") {
369
+ return;
370
+ }
371
+ try {
372
+ await this.getBackend().schedule(task, (t) => this.onTaskTick(t));
373
+ logger.warn("[TaskManager] Restored task schedule after rollback", {
374
+ taskId: task.id,
375
+ reason,
376
+ });
377
+ }
378
+ catch (restoreError) {
379
+ logger.error("[TaskManager] Failed to restore task schedule during rollback", {
380
+ taskId: task.id,
381
+ reason,
382
+ error: String(restoreError),
383
+ });
384
+ }
385
+ }
386
+ async rollbackTaskUpdate(taskId, previousTask, error) {
387
+ try {
388
+ return await this.getStore().update(taskId, previousTask);
389
+ }
390
+ catch (rollbackError) {
391
+ logger.error("[TaskManager] Failed to roll back task update — store and in-memory state may be diverged; manual reconciliation required", {
392
+ taskId,
393
+ originalError: String(error),
394
+ rollbackError: String(rollbackError),
395
+ });
396
+ throw rollbackError;
397
+ }
398
+ }
304
399
  /**
305
400
  * Called by the backend on each scheduled tick.
306
401
  * Executes the task, updates state, fires callbacks/events.
@@ -54,13 +54,17 @@ export class TelemetryService {
54
54
  if (!provider) {
55
55
  return false;
56
56
  }
57
- const delegateName = provider._delegate?.constructor?.name || "";
58
- if (delegateName && delegateName !== "NoopTracerProvider") {
57
+ const providerName = provider.constructor?.name || "";
58
+ if (providerName &&
59
+ providerName !== "ProxyTracerProvider" &&
60
+ providerName !== "NoopTracerProvider") {
59
61
  return true;
60
62
  }
61
- const providerName = provider.constructor?.name || "";
62
- return (providerName !== "ProxyTracerProvider" &&
63
- providerName !== "NoopTracerProvider");
63
+ const delegate = typeof provider.getDelegate === "function"
64
+ ? provider.getDelegate()
65
+ : provider._delegate;
66
+ const delegateName = delegate?.constructor?.name || "";
67
+ return Boolean(delegateName && delegateName !== "NoopTracerProvider");
64
68
  }
65
69
  catch (error) {
66
70
  logger.warn("[Telemetry] Failed checking for external TracerProvider", {
@@ -798,6 +798,10 @@ export type ProxyState = {
798
798
  host: string;
799
799
  strategy: string;
800
800
  startTime: string;
801
+ ready?: boolean;
802
+ readyAt?: string;
803
+ healthPath?: string;
804
+ statusPath?: string;
801
805
  envFile?: string;
802
806
  /** Fallback chain from proxy config (persisted at start time) */
803
807
  fallbackChain?: FallbackInfo[];
@@ -13,6 +13,8 @@
13
13
  * - src/lib/proxy/accountQuota.ts (quota type)
14
14
  * - src/lib/server/routes/claudeProxyRoutes.ts (runtime state, deps)
15
15
  */
16
+ import type { Span } from "@opentelemetry/api";
17
+ import type { ProxyTracer } from "../proxy/proxyTracer.js";
16
18
  /**
17
19
  * Type describing the ModelRouter contract.
18
20
  * Defined here to avoid a circular dependency between types and implementation.
@@ -242,7 +244,7 @@ export type ParsedClaudeRequest = {
242
244
  /** Tools translated to AI SDK-compatible shape for provider fallback. */
243
245
  tools: Record<string, {
244
246
  description?: string;
245
- inputSchema?: unknown;
247
+ inputSchema: unknown;
246
248
  execute?: (...args: unknown[]) => unknown;
247
249
  }>;
248
250
  /**
@@ -361,6 +363,14 @@ export type TlsFingerprintOptions = {
361
363
  /** Whether the stub should log a warning that it is a no-op. */
362
364
  warnOnUse?: boolean;
363
365
  };
366
+ /**
367
+ * Proxy operating mode:
368
+ * - "full" — managed accounts, retry, rotation, polyfill (default)
369
+ * - "passthrough" — no polyfill/retry/rotation, but body is still parsed and re-serialized
370
+ * - "transparent" — zero-mutation byte relay: raw body forwarded as-is, minimal header filtering,
371
+ * SSE interceptor for cache metrics only (bytes pass through unmodified)
372
+ */
373
+ export type ProxyMode = "full" | "passthrough" | "transparent";
364
374
  export type RouteResult = {
365
375
  provider: string | null;
366
376
  model: string;
@@ -453,6 +463,121 @@ export type RequestAttemptLogEntry = {
453
463
  /** OTel span ID for correlation with distributed traces */
454
464
  spanId?: string;
455
465
  };
466
+ export type ProxyBodyCaptureInput = {
467
+ phase: string;
468
+ headers?: Record<string, string>;
469
+ body?: unknown;
470
+ bodySize?: number;
471
+ contentType?: string;
472
+ responseStatus?: number;
473
+ durationMs?: number;
474
+ account?: string;
475
+ accountType?: string;
476
+ attempt?: number;
477
+ metadata?: Record<string, unknown>;
478
+ };
479
+ export type ProxyBodyCaptureLogger = (capture: ProxyBodyCaptureInput) => void;
480
+ export type ClaudeFinalRequestLogger = (status: number, accountLabel: string, accountType: string, errorType?: string, errorMessage?: string, extra?: {
481
+ inputTokens?: number;
482
+ outputTokens?: number;
483
+ cacheCreationTokens?: number;
484
+ cacheReadTokens?: number;
485
+ }) => void;
486
+ export type ClaudeLoggedErrorBuilder = (status: number, message: string, errorType?: string, extra?: {
487
+ account?: string;
488
+ accountType?: string;
489
+ attempt?: number;
490
+ }) => ClaudeErrorResponse;
491
+ export type ClaudeRequestRuntimeContext = {
492
+ tracer?: ProxyTracer;
493
+ requestStartTime: number;
494
+ logProxyBody: ProxyBodyCaptureLogger;
495
+ logFinalRequest: ClaudeFinalRequestLogger;
496
+ buildLoggedClaudeError: ClaudeLoggedErrorBuilder;
497
+ };
498
+ export type AnthropicAttemptLogger = (status: number, errorType?: string, errorMessage?: string, extra?: {
499
+ inputTokens?: number;
500
+ outputTokens?: number;
501
+ cacheCreationTokens?: number;
502
+ cacheReadTokens?: number;
503
+ }) => void;
504
+ export type AnthropicLoopState = {
505
+ lastError: unknown;
506
+ sawRateLimit: boolean;
507
+ sawNetworkError: boolean;
508
+ sawTransientFailure: boolean;
509
+ invalidRequestFailure: {
510
+ status: number;
511
+ body: string;
512
+ contentType?: string;
513
+ } | null;
514
+ authFailureMessage: string | null;
515
+ attemptNumber: number;
516
+ };
517
+ export type AnthropicUpstreamBody = {
518
+ bodyStr: string;
519
+ sessionId?: string;
520
+ };
521
+ export type AnthropicUpstreamBodyBuilder = (token: string) => AnthropicUpstreamBody;
522
+ export type LoadedClaudeAccountContext = {
523
+ accounts: ProxyPassthroughAccount[];
524
+ enabledAccounts: ProxyPassthroughAccount[];
525
+ orderedAccounts: ProxyPassthroughAccount[];
526
+ bodyStr: string;
527
+ requestStart: number;
528
+ toolCount: number;
529
+ url: string;
530
+ clientHeaders: Record<string, string | undefined>;
531
+ isClaudeClientRequest: boolean;
532
+ };
533
+ export type AnthropicSuccessResult = {
534
+ retryNextAccount: true;
535
+ } | {
536
+ response: Response | unknown;
537
+ };
538
+ export type AnthropicAuthRetryResult = {
539
+ response?: Response | unknown;
540
+ continueLoop: boolean;
541
+ lastError: unknown;
542
+ authFailureMessage: string | null;
543
+ sawRateLimit: boolean;
544
+ sawTransientFailure: boolean;
545
+ sawNetworkError: boolean;
546
+ upstreamSpan?: Span;
547
+ };
548
+ export type AnthropicNonOkResult = {
549
+ response?: Response | unknown;
550
+ continueLoop: boolean;
551
+ retrySameAccount?: boolean;
552
+ lastError: unknown;
553
+ authFailureMessage: string | null;
554
+ sawTransientFailure: boolean;
555
+ invalidRequestFailure: {
556
+ status: number;
557
+ body: string;
558
+ contentType?: string;
559
+ } | null;
560
+ upstreamSpan?: Span;
561
+ };
562
+ export type PreparedAnthropicAccountAttempt = {
563
+ continueLoop: boolean;
564
+ lastError: unknown;
565
+ authFailureMessage: string | null;
566
+ headers?: Record<string, string>;
567
+ buildUpstreamBody?: AnthropicUpstreamBodyBuilder;
568
+ finalBodyStr?: string;
569
+ fetchStartMs?: number;
570
+ upstreamSpan?: Span;
571
+ };
572
+ export type AnthropicUpstreamFetchResult = {
573
+ continueLoop: boolean;
574
+ retrySameAccount?: boolean;
575
+ response?: Response;
576
+ lastError: unknown;
577
+ sawRateLimit: boolean;
578
+ sawNetworkError: boolean;
579
+ upstreamSpan?: Span;
580
+ };
456
581
  export type AccountStats = {
457
582
  label: string;
458
583
  type: string;
@@ -516,6 +641,10 @@ export type RuntimeAccountState = {
516
641
  backoffLevel: number;
517
642
  consecutiveRefreshFailures: number;
518
643
  permanentlyDisabled: boolean;
644
+ requestClassCooldowns?: Record<string, number>;
645
+ modelTierCooldowns?: Record<string, number>;
646
+ requestClassBackoffLevels?: Record<string, number>;
647
+ modelTierBackoffLevels?: Record<string, number>;
519
648
  lastToken?: string;
520
649
  lastRefreshToken?: string;
521
650
  };
@@ -566,3 +695,84 @@ export type CachedSession = {
566
695
  userId: string;
567
696
  expiresAt: number;
568
697
  };
698
+ /** Model tier classification for proxy routing decisions. */
699
+ export type ClaudeProxyModelTier = "opus" | "sonnet" | "haiku" | "other";
700
+ /** Request class for proxy routing policy. */
701
+ export type ClaudeProxyRequestClass = "multimodal" | "high-tool-count-non-stream-structured" | "strong-tool-fidelity" | "streaming-conversational" | "standard";
702
+ /** Full classification profile for a proxy request. */
703
+ export type ClaudeProxyRequestProfile = {
704
+ requestedModel: string;
705
+ modelTier: ClaudeProxyModelTier;
706
+ primaryClass: ClaudeProxyRequestClass;
707
+ classes: ClaudeProxyRequestClass[];
708
+ stream: boolean;
709
+ toolCount: number;
710
+ hasImages: boolean;
711
+ hasThinking: boolean;
712
+ hasToolHistory: boolean;
713
+ requiresToolUse: boolean;
714
+ requiresSpecificTool: boolean;
715
+ requiresStrongToolFidelity: boolean;
716
+ isHighToolCountNonStream: boolean;
717
+ isStreamingConversational: boolean;
718
+ isMultimodal: boolean;
719
+ };
720
+ /** Outcome of evaluating a single fallback candidate. */
721
+ export type FallbackEligibilityDecision = {
722
+ provider?: string;
723
+ model?: string;
724
+ eligible: boolean;
725
+ reason: string;
726
+ };
727
+ /** A single provider attempt in the proxy translation plan. */
728
+ export type ProxyTranslationAttempt = {
729
+ provider?: string;
730
+ model?: string;
731
+ label: string;
732
+ };
733
+ /** Ordered plan of provider attempts and skipped candidates. */
734
+ export type ProxyTranslationPlan = {
735
+ profile: ClaudeProxyRequestProfile;
736
+ attempts: ProxyTranslationAttempt[];
737
+ skipped: FallbackEligibilityDecision[];
738
+ };
739
+ /** Discriminated union describing why a cooldown is active. */
740
+ export type CooldownScope = {
741
+ scope: "request_class";
742
+ key: string;
743
+ until: number;
744
+ } | {
745
+ scope: "model_tier";
746
+ key: string;
747
+ until: number;
748
+ } | {
749
+ scope: "generic";
750
+ key: "generic";
751
+ until: number;
752
+ };
753
+ /** An account skipped during partitioning, with the cooldown that caused it. */
754
+ export type CooldownSkippedAccount<T> = {
755
+ account: T;
756
+ cooldown: CooldownScope;
757
+ };
758
+ /** Mutable readiness state tracked by the proxy process. */
759
+ export type ProxyReadinessState = {
760
+ startTimeMs: number;
761
+ acceptingConnections: boolean;
762
+ ready: boolean;
763
+ readyAtMs?: number;
764
+ };
765
+ /** Structured response returned by the proxy /health endpoint. */
766
+ export type ProxyHealthResponse = {
767
+ status: "ok" | "starting";
768
+ ready: boolean;
769
+ acceptingConnections: boolean;
770
+ strategy: string;
771
+ passthrough: boolean;
772
+ version: string;
773
+ startedAt: string;
774
+ readyAt: string | null;
775
+ uptime: number;
776
+ healthPath: "/health";
777
+ statusPath: "/status";
778
+ };
@@ -294,6 +294,8 @@ export type ToolExecutionContext = {
294
294
  export type ToolExecutionEvent = {
295
295
  type: "tool:start" | "tool:end";
296
296
  tool: string;
297
+ /** Compatibility alias for older consumers that expect `toolName`. */
298
+ toolName?: string;
297
299
  input?: unknown;
298
300
  result?: unknown;
299
301
  error?: string;
@@ -301,6 +303,22 @@ export type ToolExecutionEvent = {
301
303
  duration?: number;
302
304
  executionId: string;
303
305
  };
306
+ /**
307
+ * Payload emitted for tool:start and tool:end events.
308
+ * Always includes both `tool` and `toolName` for backward compatibility.
309
+ */
310
+ export type ToolEventPayload = {
311
+ tool: string;
312
+ toolName: string;
313
+ input?: unknown;
314
+ result?: unknown;
315
+ error?: string;
316
+ success?: boolean;
317
+ responseTime?: number;
318
+ timestamp?: number;
319
+ duration?: number;
320
+ executionId?: string;
321
+ };
304
322
  /**
305
323
  * Tool execution summary for completed executions
306
324
  */
@@ -30,6 +30,7 @@ export declare class ProviderHealthChecker {
30
30
  * Check connectivity to provider endpoints
31
31
  */
32
32
  private static checkConnectivity;
33
+ private static getConnectivityHeaders;
33
34
  /**
34
35
  * Check model availability (if possible without making API calls)
35
36
  */
@@ -72,7 +72,7 @@ export class ProviderHealthChecker {
72
72
  };
73
73
  try {
74
74
  // 1. Check environment configuration
75
- await this.checkEnvironmentConfiguration(providerName, healthStatus);
75
+ await this.checkEnvironmentConfiguration(providerName, healthStatus, timeout);
76
76
  // 2. Check API key validity (basic format validation)
77
77
  await this.checkApiKeyValidity(providerName, healthStatus);
78
78
  // 3. Optional: Connectivity test
@@ -129,7 +129,7 @@ export class ProviderHealthChecker {
129
129
  /**
130
130
  * Check environment configuration for a provider
131
131
  */
132
- static async checkEnvironmentConfiguration(providerName, healthStatus) {
132
+ static async checkEnvironmentConfiguration(providerName, healthStatus, timeout) {
133
133
  const requiredEnvVars = this.getRequiredEnvironmentVariables(providerName);
134
134
  logger.debug(`[ProviderHealthChecker] Checking environment configuration for ${providerName}`, {
135
135
  requiredEnvVars,
@@ -160,7 +160,7 @@ export class ProviderHealthChecker {
160
160
  healthStatus.recommendations.push(`Set the following environment variables: ${missingVars.join(", ")}`);
161
161
  }
162
162
  // Provider-specific configuration checks
163
- await this.checkProviderSpecificConfig(providerName, healthStatus);
163
+ await this.checkProviderSpecificConfig(providerName, healthStatus, timeout);
164
164
  }
165
165
  /**
166
166
  * Check API key validity (format validation)
@@ -261,30 +261,34 @@ export class ProviderHealthChecker {
261
261
  healthStatus.warning = "No connectivity test available for this provider";
262
262
  return;
263
263
  }
264
+ const headers = {
265
+ "User-Agent": "NeuroLink-HealthCheck/1.0",
266
+ ...this.getConnectivityHeaders(providerName),
267
+ };
264
268
  try {
265
269
  const controller = new AbortController();
266
270
  const timeoutId = setTimeout(() => controller.abort(), timeout);
267
- const proxyFetch = createProxyFetch();
268
- let response = await proxyFetch(endpoint, {
269
- method: "HEAD",
270
- signal: controller.signal,
271
- headers: {
272
- "User-Agent": "NeuroLink-HealthCheck/1.0",
273
- },
274
- });
275
- // Fallback to GET if HEAD returns 405 (Method Not Allowed) for restrictive gateways
276
- if (response.status === 405) {
277
- response = await proxyFetch(endpoint, {
278
- method: "GET",
271
+ try {
272
+ const proxyFetch = createProxyFetch();
273
+ let response = await proxyFetch(endpoint, {
274
+ method: "HEAD",
279
275
  signal: controller.signal,
280
- headers: {
281
- "User-Agent": "NeuroLink-HealthCheck/1.0",
282
- },
276
+ headers,
283
277
  });
278
+ // Fallback to GET if HEAD returns 405 (Method Not Allowed) for restrictive gateways
279
+ if (response.status === 405) {
280
+ response = await proxyFetch(endpoint, {
281
+ method: "GET",
282
+ signal: controller.signal,
283
+ headers,
284
+ });
285
+ }
286
+ if (!response.ok) {
287
+ healthStatus.configurationIssues.push(`Connectivity test failed: HTTP ${response.status}`);
288
+ }
284
289
  }
285
- clearTimeout(timeoutId);
286
- if (!response.ok) {
287
- healthStatus.configurationIssues.push(`Connectivity test failed: HTTP ${response.status}`);
290
+ finally {
291
+ clearTimeout(timeoutId);
288
292
  }
289
293
  }
290
294
  catch (error) {
@@ -320,6 +324,14 @@ export class ProviderHealthChecker {
320
324
  }
321
325
  }
322
326
  }
327
+ static getConnectivityHeaders(providerName) {
328
+ if (providerName === AIProviderName.LITELLM) {
329
+ return {
330
+ Authorization: `Bearer ${process.env.LITELLM_API_KEY || "sk-anything"}`,
331
+ };
332
+ }
333
+ return {};
334
+ }
323
335
  /**
324
336
  * Check model availability (if possible without making API calls)
325
337
  */
@@ -455,7 +467,7 @@ export class ProviderHealthChecker {
455
467
  /**
456
468
  * Provider-specific configuration checks
457
469
  */
458
- static async checkProviderSpecificConfig(providerName, healthStatus) {
470
+ static async checkProviderSpecificConfig(providerName, healthStatus, timeout) {
459
471
  switch (providerName) {
460
472
  case AIProviderName.VERTEX:
461
473
  await this.checkVertexAIConfig(healthStatus);
@@ -467,10 +479,10 @@ export class ProviderHealthChecker {
467
479
  await this.checkAzureConfig(healthStatus);
468
480
  break;
469
481
  case AIProviderName.LITELLM:
470
- await this.checkLiteLLMConfig(healthStatus);
482
+ await this.checkLiteLLMConfig(healthStatus, timeout);
471
483
  break;
472
484
  case AIProviderName.OLLAMA:
473
- await this.checkOllamaConfig(healthStatus);
485
+ await this.checkOllamaConfig(healthStatus, timeout);
474
486
  break;
475
487
  }
476
488
  }
@@ -722,9 +734,12 @@ export class ProviderHealthChecker {
722
734
  .filter((model) => typeof model === "string");
723
735
  }
724
736
  static hasRequestedModel(availableModels, requestedModel) {
725
- return availableModels.some((model) => model === requestedModel ||
726
- model.startsWith(`${requestedModel}:`) ||
727
- requestedModel.startsWith(`${model}:`));
737
+ const normalizedRequestedModel = requestedModel.trim();
738
+ const requiresExactMatch = /@/.test(normalizedRequestedModel);
739
+ return availableModels.some((model) => model === normalizedRequestedModel ||
740
+ (!requiresExactMatch &&
741
+ (model.startsWith(`${normalizedRequestedModel}:`) ||
742
+ model.startsWith(`${normalizedRequestedModel}@`))));
728
743
  }
729
744
  static async getOllamaAvailableModels(timeout = 2000) {
730
745
  const payload = (await this.fetchJsonWithTimeout(this.getOllamaTagsUrl(), {
@@ -789,7 +804,7 @@ export class ProviderHealthChecker {
789
804
  };
790
805
  }
791
806
  }
792
- static async checkLiteLLMConfig(healthStatus) {
807
+ static async checkLiteLLMConfig(healthStatus, timeout = this.DEFAULT_TIMEOUT) {
793
808
  const liteLLMBase = this.getLiteLLMBaseUrl();
794
809
  if (!liteLLMBase.startsWith("http")) {
795
810
  healthStatus.isConfigured = false;
@@ -799,7 +814,7 @@ export class ProviderHealthChecker {
799
814
  }
800
815
  const availability = await this.checkLiteLLMAvailability({
801
816
  model: this.getConfiguredLiteLLMModel(),
802
- timeout: 2000,
817
+ timeout,
803
818
  });
804
819
  if (!availability.available) {
805
820
  healthStatus.isConfigured = false;
@@ -812,7 +827,7 @@ export class ProviderHealthChecker {
812
827
  /**
813
828
  * Check Ollama configuration
814
829
  */
815
- static async checkOllamaConfig(healthStatus) {
830
+ static async checkOllamaConfig(healthStatus, timeout = this.DEFAULT_TIMEOUT) {
816
831
  const ollamaBase = this.getOllamaBaseUrl();
817
832
  if (!ollamaBase.startsWith("http")) {
818
833
  healthStatus.isConfigured = false;
@@ -822,7 +837,7 @@ export class ProviderHealthChecker {
822
837
  }
823
838
  const availability = await this.checkOllamaAvailability({
824
839
  model: this.getConfiguredOllamaModel(),
825
- timeout: 2000,
840
+ timeout,
826
841
  });
827
842
  if (!availability.available) {
828
843
  healthStatus.isConfigured = false;