@juspay/neurolink 9.42.0 → 9.42.1
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 +2 -0
- package/dist/auth/anthropicOAuth.js +12 -0
- package/dist/browser/neurolink.min.js +337 -336
- package/dist/cli/commands/mcp.d.ts +6 -0
- package/dist/cli/commands/mcp.js +188 -184
- package/dist/cli/commands/proxy.js +537 -518
- package/dist/core/baseProvider.d.ts +6 -1
- package/dist/core/baseProvider.js +208 -230
- package/dist/core/factory.d.ts +3 -0
- package/dist/core/factory.js +138 -188
- package/dist/evaluation/pipeline/evaluationPipeline.js +5 -2
- package/dist/evaluation/scorers/scorerRegistry.d.ts +3 -0
- package/dist/evaluation/scorers/scorerRegistry.js +353 -282
- package/dist/lib/auth/anthropicOAuth.js +12 -0
- package/dist/lib/core/baseProvider.d.ts +6 -1
- package/dist/lib/core/baseProvider.js +208 -230
- package/dist/lib/core/factory.d.ts +3 -0
- package/dist/lib/core/factory.js +138 -188
- package/dist/lib/evaluation/pipeline/evaluationPipeline.js +5 -2
- package/dist/lib/evaluation/scorers/scorerRegistry.d.ts +3 -0
- package/dist/lib/evaluation/scorers/scorerRegistry.js +353 -282
- package/dist/lib/mcp/toolRegistry.d.ts +2 -0
- package/dist/lib/mcp/toolRegistry.js +32 -31
- package/dist/lib/neurolink.d.ts +38 -0
- package/dist/lib/neurolink.js +1858 -1689
- package/dist/lib/providers/googleAiStudio.js +0 -5
- package/dist/lib/providers/googleVertex.d.ts +10 -0
- package/dist/lib/providers/googleVertex.js +436 -444
- package/dist/lib/providers/litellm.d.ts +1 -0
- package/dist/lib/providers/litellm.js +73 -64
- package/dist/lib/providers/ollama.js +17 -4
- package/dist/lib/providers/openAI.d.ts +2 -0
- package/dist/lib/providers/openAI.js +139 -140
- package/dist/lib/proxy/claudeFormat.js +12 -4
- package/dist/lib/proxy/oauthFetch.js +298 -318
- package/dist/lib/proxy/proxyConfig.js +3 -1
- package/dist/lib/proxy/proxyFetch.js +250 -222
- package/dist/lib/proxy/requestLogger.js +132 -45
- package/dist/lib/proxy/sseInterceptor.js +36 -11
- package/dist/lib/server/routes/claudeProxyRoutes.d.ts +10 -1
- package/dist/lib/server/routes/claudeProxyRoutes.js +2726 -2272
- package/dist/lib/services/server/ai/observability/instrumentation.js +194 -218
- package/dist/lib/tasks/backends/bullmqBackend.js +24 -18
- package/dist/lib/tasks/store/redisTaskStore.js +23 -16
- package/dist/lib/tasks/taskManager.d.ts +2 -0
- package/dist/lib/tasks/taskManager.js +100 -5
- package/dist/lib/telemetry/telemetryService.js +9 -5
- package/dist/lib/types/proxyTypes.d.ts +124 -1
- package/dist/lib/utils/providerHealth.d.ts +1 -0
- package/dist/lib/utils/providerHealth.js +46 -31
- package/dist/lib/utils/providerUtils.js +11 -22
- package/dist/mcp/toolRegistry.d.ts +2 -0
- package/dist/mcp/toolRegistry.js +32 -31
- package/dist/neurolink.d.ts +38 -0
- package/dist/neurolink.js +1858 -1689
- package/dist/providers/googleAiStudio.js +0 -5
- package/dist/providers/googleVertex.d.ts +10 -0
- package/dist/providers/googleVertex.js +436 -444
- package/dist/providers/litellm.d.ts +1 -0
- package/dist/providers/litellm.js +73 -64
- package/dist/providers/ollama.js +17 -4
- package/dist/providers/openAI.d.ts +2 -0
- package/dist/providers/openAI.js +139 -140
- package/dist/proxy/claudeFormat.js +12 -4
- package/dist/proxy/oauthFetch.js +298 -318
- package/dist/proxy/proxyConfig.js +3 -1
- package/dist/proxy/proxyFetch.js +250 -222
- package/dist/proxy/requestLogger.js +132 -45
- package/dist/proxy/sseInterceptor.js +36 -11
- package/dist/server/routes/claudeProxyRoutes.d.ts +10 -1
- package/dist/server/routes/claudeProxyRoutes.js +2726 -2272
- package/dist/services/server/ai/observability/instrumentation.js +194 -218
- package/dist/tasks/backends/bullmqBackend.js +24 -18
- package/dist/tasks/store/redisTaskStore.js +23 -16
- package/dist/tasks/taskManager.d.ts +2 -0
- package/dist/tasks/taskManager.js +100 -5
- package/dist/telemetry/telemetryService.js +9 -5
- package/dist/types/proxyTypes.d.ts +124 -1
- package/dist/utils/providerHealth.d.ts +1 -0
- package/dist/utils/providerHealth.js +46 -31
- package/dist/utils/providerUtils.js +12 -22
- package/package.json +3 -2
- package/scripts/observability/check-proxy-telemetry.mjs +1 -1
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
58
|
-
if (
|
|
57
|
+
const providerName = provider.constructor?.name || "";
|
|
58
|
+
if (providerName &&
|
|
59
|
+
providerName !== "ProxyTracerProvider" &&
|
|
60
|
+
providerName !== "NoopTracerProvider") {
|
|
59
61
|
return true;
|
|
60
62
|
}
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
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", {
|
|
@@ -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
|
|
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,119 @@ 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
|
+
lastError: unknown;
|
|
552
|
+
authFailureMessage: string | null;
|
|
553
|
+
sawTransientFailure: boolean;
|
|
554
|
+
invalidRequestFailure: {
|
|
555
|
+
status: number;
|
|
556
|
+
body: string;
|
|
557
|
+
contentType?: string;
|
|
558
|
+
} | null;
|
|
559
|
+
upstreamSpan?: Span;
|
|
560
|
+
};
|
|
561
|
+
export type PreparedAnthropicAccountAttempt = {
|
|
562
|
+
continueLoop: boolean;
|
|
563
|
+
lastError: unknown;
|
|
564
|
+
authFailureMessage: string | null;
|
|
565
|
+
headers?: Record<string, string>;
|
|
566
|
+
buildUpstreamBody?: AnthropicUpstreamBodyBuilder;
|
|
567
|
+
finalBodyStr?: string;
|
|
568
|
+
fetchStartMs?: number;
|
|
569
|
+
upstreamSpan?: Span;
|
|
570
|
+
};
|
|
571
|
+
export type AnthropicUpstreamFetchResult = {
|
|
572
|
+
continueLoop: boolean;
|
|
573
|
+
response?: Response;
|
|
574
|
+
lastError: unknown;
|
|
575
|
+
sawRateLimit: boolean;
|
|
576
|
+
sawNetworkError: boolean;
|
|
577
|
+
upstreamSpan?: Span;
|
|
578
|
+
};
|
|
456
579
|
export type AccountStats = {
|
|
457
580
|
label: string;
|
|
458
581
|
type: string;
|
|
@@ -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
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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
|
-
|
|
286
|
-
|
|
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
|
-
|
|
726
|
-
|
|
727
|
-
|
|
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
|
|
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
|
|
840
|
+
timeout,
|
|
826
841
|
});
|
|
827
842
|
if (!availability.available) {
|
|
828
843
|
healthStatus.isConfigured = false;
|
|
@@ -50,7 +50,8 @@ export async function getBestProvider(requestedProvider) {
|
|
|
50
50
|
return process.env.DEFAULT_PROVIDER;
|
|
51
51
|
}
|
|
52
52
|
// Special case for Ollama - prioritize local when available
|
|
53
|
-
if (process.env.OLLAMA_BASE_URL
|
|
53
|
+
if ((process.env.OLLAMA_BASE_URL || process.env.OLLAMA_API_BASE) &&
|
|
54
|
+
process.env.OLLAMA_MODEL) {
|
|
54
55
|
try {
|
|
55
56
|
if (await isProviderAvailable("ollama")) {
|
|
56
57
|
logger.debug(`[getBestProvider] Prioritizing working local Ollama`);
|
|
@@ -64,7 +65,7 @@ export async function getBestProvider(requestedProvider) {
|
|
|
64
65
|
/**
|
|
65
66
|
* Provider priority order rationale:
|
|
66
67
|
* - LiteLLM and Ollama are prioritized first for local/self-hosted deployments,
|
|
67
|
-
* avoiding
|
|
68
|
+
* avoiding unnecessary dependence on external providers during fallback scenarios.
|
|
68
69
|
* - Vertex (Google Cloud AI) follows for enterprise-grade reliability.
|
|
69
70
|
* - Google AI follows as second cloud priority for comprehensive Google AI ecosystem support.
|
|
70
71
|
* - OpenAI maintains high priority due to its consistent reliability and broad model support.
|
|
@@ -72,8 +73,8 @@ export async function getBestProvider(requestedProvider) {
|
|
|
72
73
|
* Please update this comment if the order is changed in the future, and document the rationale for maintainability.
|
|
73
74
|
*/
|
|
74
75
|
const providers = [
|
|
75
|
-
"litellm", // Prioritize self-hosted
|
|
76
|
-
"ollama", // Local models
|
|
76
|
+
"litellm", // Prioritize self-hosted proxy deployments first
|
|
77
|
+
"ollama", // Local models when the configured runtime target is installed
|
|
77
78
|
"vertex", // Google Cloud AI (enterprise)
|
|
78
79
|
"google-ai", // Google AI ecosystem support
|
|
79
80
|
"openai", // Reliable with broad model support
|
|
@@ -101,25 +102,13 @@ async function isProviderAvailable(providerName) {
|
|
|
101
102
|
if (!hasProviderEnvVars(providerName) && providerName !== "ollama") {
|
|
102
103
|
return false;
|
|
103
104
|
}
|
|
105
|
+
if (providerName === "litellm") {
|
|
106
|
+
const availability = await ProviderHealthChecker.checkFallbackProviderAvailability(AIProviderName.LITELLM, process.env.LITELLM_MODEL || "openai/gpt-4o-mini");
|
|
107
|
+
return availability.available;
|
|
108
|
+
}
|
|
104
109
|
if (providerName === "ollama") {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
method: "GET",
|
|
108
|
-
signal: AbortSignal.timeout(2000),
|
|
109
|
-
});
|
|
110
|
-
if (response.ok) {
|
|
111
|
-
const { models } = await response.json();
|
|
112
|
-
const defaultOllamaModel = process.env.OLLAMA_MODEL || "llama3.1:8b";
|
|
113
|
-
// Check for exact match first, then prefix match (e.g. "gemma3:27b" matches "gemma3:27b-fp16")
|
|
114
|
-
return models.some((m) => m.name === defaultOllamaModel ||
|
|
115
|
-
(typeof m.name === "string" &&
|
|
116
|
-
m.name.startsWith(defaultOllamaModel.split(":")[0] + ":")));
|
|
117
|
-
}
|
|
118
|
-
return false;
|
|
119
|
-
}
|
|
120
|
-
catch {
|
|
121
|
-
return false;
|
|
122
|
-
}
|
|
110
|
+
const availability = await ProviderHealthChecker.checkFallbackProviderAvailability(AIProviderName.OLLAMA, process.env.OLLAMA_MODEL || "llama3.1:8b");
|
|
111
|
+
return availability.available;
|
|
123
112
|
}
|
|
124
113
|
try {
|
|
125
114
|
const provider = await AIProviderFactory.createProvider(providerName);
|
|
@@ -69,6 +69,8 @@ export declare class MCPToolRegistry extends MCPRegistry {
|
|
|
69
69
|
permissions?: string[];
|
|
70
70
|
context?: ExecutionContext;
|
|
71
71
|
}): Promise<ToolInfo[]>;
|
|
72
|
+
private resolveToolExecutionTarget;
|
|
73
|
+
private createExecutionContext;
|
|
72
74
|
/**
|
|
73
75
|
* Get tool information with server details
|
|
74
76
|
*/
|
package/dist/mcp/toolRegistry.js
CHANGED
|
@@ -9,6 +9,7 @@ import { shouldDisableBuiltinTools } from "../utils/toolUtils.js";
|
|
|
9
9
|
import { directAgentTools } from "../agent/directTools.js";
|
|
10
10
|
import { detectCategory, createMCPServerInfo } from "../utils/mcpDefaults.js";
|
|
11
11
|
import { FlexibleToolValidator } from "./flexibleToolValidator.js";
|
|
12
|
+
import { ErrorFactory } from "../utils/errorHandling.js";
|
|
12
13
|
import { HITLUserRejectedError, HITLTimeoutError } from "../hitl/hitlErrors.js";
|
|
13
14
|
import { withSpan, tracers, ATTR } from "../telemetry/index.js";
|
|
14
15
|
import { getAuthContext } from "../auth/authContext.js";
|
|
@@ -263,22 +264,7 @@ export class MCPToolRegistry extends MCPRegistry {
|
|
|
263
264
|
hasContext: context !== undefined,
|
|
264
265
|
sessionId: context?.sessionId,
|
|
265
266
|
});
|
|
266
|
-
|
|
267
|
-
let tool = this.tools.get(toolName);
|
|
268
|
-
registryLogger.info(`🔍 [TOOL_LOOKUP] Direct lookup result for '${toolName}':`, !!tool);
|
|
269
|
-
// If not found, search for tool by name across all entries (for backward compatibility)
|
|
270
|
-
let toolId = toolName;
|
|
271
|
-
if (!tool) {
|
|
272
|
-
const matches = Array.from(this.tools.entries()).filter(([, toolInfo]) => toolInfo.name === toolName);
|
|
273
|
-
if (matches.length > 1) {
|
|
274
|
-
throw new Error(`Ambiguous tool name '${toolName}'. Use fully-qualified name 'serverId.${toolName}'.`);
|
|
275
|
-
}
|
|
276
|
-
if (matches.length === 1) {
|
|
277
|
-
const [candidateToolId, toolInfo] = matches[0];
|
|
278
|
-
tool = toolInfo;
|
|
279
|
-
toolId = candidateToolId;
|
|
280
|
-
}
|
|
281
|
-
}
|
|
267
|
+
const { tool, toolId } = this.resolveToolExecutionTarget(toolName);
|
|
282
268
|
if (!tool) {
|
|
283
269
|
throw new Error(`Tool '${toolName}' not found in registry`);
|
|
284
270
|
}
|
|
@@ -291,21 +277,7 @@ export class MCPToolRegistry extends MCPRegistry {
|
|
|
291
277
|
: "mcp";
|
|
292
278
|
span.setAttribute("tool.type", toolType);
|
|
293
279
|
span.setAttribute(ATTR.MCP_SERVER_ID, serverId);
|
|
294
|
-
|
|
295
|
-
let authUserId;
|
|
296
|
-
try {
|
|
297
|
-
const authCtx = getAuthContext();
|
|
298
|
-
authUserId = authCtx?.user?.id;
|
|
299
|
-
}
|
|
300
|
-
catch {
|
|
301
|
-
// Auth context not available — that's fine
|
|
302
|
-
}
|
|
303
|
-
// Create execution context if not provided
|
|
304
|
-
const execContext = {
|
|
305
|
-
...context,
|
|
306
|
-
sessionId: context?.sessionId ?? randomUUID(),
|
|
307
|
-
userId: context?.userId ?? authUserId,
|
|
308
|
-
};
|
|
280
|
+
const execContext = this.createExecutionContext(context);
|
|
309
281
|
// Get the tool implementation using the resolved toolId
|
|
310
282
|
const toolImpl = this.toolImplementations.get(toolId);
|
|
311
283
|
registryLogger.debug(`Looking for tool '${toolName}' (toolId: '${toolId}'), found: ${!!toolImpl}, type: ${typeof toolImpl?.execute}`);
|
|
@@ -504,6 +476,35 @@ export class MCPToolRegistry extends MCPRegistry {
|
|
|
504
476
|
registryLogger.debug(`Listed ${result.length} unique tools (${filter ? "filtered" : "unfiltered"})`);
|
|
505
477
|
return result;
|
|
506
478
|
}
|
|
479
|
+
resolveToolExecutionTarget(toolName) {
|
|
480
|
+
let tool = this.tools.get(toolName);
|
|
481
|
+
registryLogger.info(`🔍 [TOOL_LOOKUP] Direct lookup result for '${toolName}':`, !!tool);
|
|
482
|
+
let toolId = toolName;
|
|
483
|
+
if (!tool) {
|
|
484
|
+
const matches = Array.from(this.tools.entries()).filter(([, toolInfo]) => toolInfo.name === toolName);
|
|
485
|
+
if (matches.length > 1) {
|
|
486
|
+
throw ErrorFactory.toolExecutionFailed(toolName, new Error(`Ambiguous tool name '${toolName}'. Use fully-qualified name 'serverId.${toolName}'.`));
|
|
487
|
+
}
|
|
488
|
+
if (matches.length === 1) {
|
|
489
|
+
[toolId, tool] = matches[0];
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
return { tool, toolId };
|
|
493
|
+
}
|
|
494
|
+
createExecutionContext(context) {
|
|
495
|
+
let authUserId;
|
|
496
|
+
try {
|
|
497
|
+
authUserId = getAuthContext()?.user?.id;
|
|
498
|
+
}
|
|
499
|
+
catch {
|
|
500
|
+
// Auth context not available — that's fine
|
|
501
|
+
}
|
|
502
|
+
return {
|
|
503
|
+
...context,
|
|
504
|
+
sessionId: context?.sessionId ?? randomUUID(),
|
|
505
|
+
userId: context?.userId ?? authUserId,
|
|
506
|
+
};
|
|
507
|
+
}
|
|
507
508
|
/**
|
|
508
509
|
* Get tool information with server details
|
|
509
510
|
*/
|