@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.
- package/CHANGELOG.md +8 -0
- package/dist/auth/anthropicOAuth.js +12 -0
- package/dist/browser/neurolink.min.js +335 -334
- package/dist/cli/commands/mcp.d.ts +6 -0
- package/dist/cli/commands/mcp.js +200 -184
- package/dist/cli/commands/proxy.js +560 -518
- package/dist/core/baseProvider.d.ts +6 -1
- package/dist/core/baseProvider.js +219 -232
- package/dist/core/factory.d.ts +3 -0
- package/dist/core/factory.js +140 -190
- package/dist/core/modules/ToolsManager.d.ts +1 -0
- package/dist/core/modules/ToolsManager.js +40 -42
- package/dist/core/toolEvents.d.ts +3 -0
- package/dist/core/toolEvents.js +7 -0
- package/dist/evaluation/pipeline/evaluationPipeline.js +5 -2
- package/dist/evaluation/scorers/scorerRegistry.d.ts +3 -0
- package/dist/evaluation/scorers/scorerRegistry.js +356 -284
- package/dist/lib/auth/anthropicOAuth.js +12 -0
- package/dist/lib/core/baseProvider.d.ts +6 -1
- package/dist/lib/core/baseProvider.js +219 -232
- package/dist/lib/core/factory.d.ts +3 -0
- package/dist/lib/core/factory.js +140 -190
- package/dist/lib/core/modules/ToolsManager.d.ts +1 -0
- package/dist/lib/core/modules/ToolsManager.js +40 -42
- package/dist/lib/core/toolEvents.d.ts +3 -0
- package/dist/lib/core/toolEvents.js +8 -0
- 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 +356 -284
- 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 +1890 -1707
- package/dist/lib/providers/googleAiStudio.js +0 -5
- package/dist/lib/providers/googleNativeGemini3.d.ts +4 -0
- package/dist/lib/providers/googleNativeGemini3.js +39 -1
- package/dist/lib/providers/googleVertex.d.ts +10 -0
- package/dist/lib/providers/googleVertex.js +445 -445
- 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 +14 -5
- 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/proxyHealth.d.ts +17 -0
- package/dist/lib/proxy/proxyHealth.js +55 -0
- package/dist/lib/proxy/requestLogger.js +140 -48
- package/dist/lib/proxy/routingPolicy.d.ts +33 -0
- package/dist/lib/proxy/routingPolicy.js +255 -0
- package/dist/lib/proxy/snapshotPersistence.d.ts +2 -0
- package/dist/lib/proxy/snapshotPersistence.js +41 -0
- package/dist/lib/proxy/sseInterceptor.js +36 -11
- package/dist/lib/server/routes/claudeProxyRoutes.d.ts +2 -1
- package/dist/lib/server/routes/claudeProxyRoutes.js +2916 -2377
- 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 +42 -17
- 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/cli.d.ts +4 -0
- package/dist/lib/types/proxyTypes.d.ts +211 -1
- package/dist/lib/types/tools.d.ts +18 -0
- 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/lib/utils/schemaConversion.d.ts +1 -0
- package/dist/lib/utils/schemaConversion.js +3 -0
- 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 +1890 -1707
- package/dist/providers/googleAiStudio.js +0 -5
- package/dist/providers/googleNativeGemini3.d.ts +4 -0
- package/dist/providers/googleNativeGemini3.js +39 -1
- package/dist/providers/googleVertex.d.ts +10 -0
- package/dist/providers/googleVertex.js +445 -445
- 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 +14 -5
- package/dist/proxy/oauthFetch.js +298 -318
- package/dist/proxy/proxyConfig.js +3 -1
- package/dist/proxy/proxyFetch.js +250 -222
- package/dist/proxy/proxyHealth.d.ts +17 -0
- package/dist/proxy/proxyHealth.js +54 -0
- package/dist/proxy/requestLogger.js +140 -48
- package/dist/proxy/routingPolicy.d.ts +33 -0
- package/dist/proxy/routingPolicy.js +254 -0
- package/dist/proxy/snapshotPersistence.d.ts +2 -0
- package/dist/proxy/snapshotPersistence.js +40 -0
- package/dist/proxy/sseInterceptor.js +36 -11
- package/dist/server/routes/claudeProxyRoutes.d.ts +2 -1
- package/dist/server/routes/claudeProxyRoutes.js +2916 -2377
- package/dist/services/server/ai/observability/instrumentation.js +194 -218
- package/dist/tasks/backends/bullmqBackend.js +24 -18
- package/dist/tasks/store/redisTaskStore.js +42 -17
- 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/cli.d.ts +4 -0
- package/dist/types/proxyTypes.d.ts +211 -1
- package/dist/types/tools.d.ts +18 -0
- package/dist/utils/providerHealth.d.ts +1 -0
- package/dist/utils/providerHealth.js +46 -31
- package/dist/utils/providerUtils.js +12 -22
- package/dist/utils/schemaConversion.d.ts +1 -0
- package/dist/utils/schemaConversion.js +3 -0
- 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", {
|
package/dist/lib/types/cli.d.ts
CHANGED
|
@@ -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
|
|
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
|
*/
|
|
@@ -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;
|