@juspay/neurolink 9.42.1 → 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 +6 -0
- package/dist/browser/neurolink.min.js +300 -300
- package/dist/cli/commands/mcp.js +15 -3
- package/dist/cli/commands/proxy.js +29 -6
- package/dist/core/baseProvider.js +12 -3
- package/dist/core/factory.js +4 -4
- 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/scorers/scorerRegistry.js +3 -2
- package/dist/lib/core/baseProvider.js +12 -3
- package/dist/lib/core/factory.js +4 -4
- 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/scorers/scorerRegistry.js +3 -2
- package/dist/lib/neurolink.js +33 -19
- package/dist/lib/providers/googleNativeGemini3.d.ts +4 -0
- package/dist/lib/providers/googleNativeGemini3.js +39 -1
- package/dist/lib/providers/googleVertex.js +10 -2
- package/dist/lib/proxy/claudeFormat.js +2 -1
- package/dist/lib/proxy/proxyHealth.d.ts +17 -0
- package/dist/lib/proxy/proxyHealth.js +55 -0
- package/dist/lib/proxy/requestLogger.js +8 -3
- 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/server/routes/claudeProxyRoutes.d.ts +1 -9
- package/dist/lib/server/routes/claudeProxyRoutes.js +304 -219
- package/dist/lib/tasks/store/redisTaskStore.js +34 -16
- package/dist/lib/types/cli.d.ts +4 -0
- package/dist/lib/types/proxyTypes.d.ts +87 -0
- package/dist/lib/types/tools.d.ts +18 -0
- package/dist/lib/utils/schemaConversion.d.ts +1 -0
- package/dist/lib/utils/schemaConversion.js +3 -0
- package/dist/neurolink.js +33 -19
- package/dist/providers/googleNativeGemini3.d.ts +4 -0
- package/dist/providers/googleNativeGemini3.js +39 -1
- package/dist/providers/googleVertex.js +10 -2
- package/dist/proxy/claudeFormat.js +2 -1
- package/dist/proxy/proxyHealth.d.ts +17 -0
- package/dist/proxy/proxyHealth.js +54 -0
- package/dist/proxy/requestLogger.js +8 -3
- 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/server/routes/claudeProxyRoutes.d.ts +1 -9
- package/dist/server/routes/claudeProxyRoutes.js +304 -219
- package/dist/tasks/store/redisTaskStore.js +34 -16
- package/dist/types/cli.d.ts +4 -0
- package/dist/types/proxyTypes.d.ts +87 -0
- package/dist/types/tools.d.ts +18 -0
- package/dist/utils/schemaConversion.d.ts +1 -0
- package/dist/utils/schemaConversion.js +3 -0
- package/package.json +1 -1
package/dist/lib/neurolink.js
CHANGED
|
@@ -28,6 +28,7 @@ import { getContextOverflowProvider, isContextOverflowError, parseProviderOverfl
|
|
|
28
28
|
import { ContextBudgetExceededError } from "./context/errors.js";
|
|
29
29
|
import { repairToolPairs } from "./context/toolPairRepair.js";
|
|
30
30
|
import { SYSTEM_LIMITS } from "./core/constants.js";
|
|
31
|
+
import { createToolEventPayload } from "./core/toolEvents.js";
|
|
31
32
|
import { ConversationMemoryManager } from "./core/conversationMemoryManager.js";
|
|
32
33
|
import { AIProviderFactory } from "./core/factory.js";
|
|
33
34
|
import { ProviderRegistry } from "./factories/providerRegistry.js";
|
|
@@ -236,14 +237,13 @@ export class NeuroLink {
|
|
|
236
237
|
// Emit tool end event (NeuroLink format - enhanced with result/error)
|
|
237
238
|
// Serialize error to string for consumer compatibility (event listeners
|
|
238
239
|
// commonly check `typeof event.error === "string"`).
|
|
239
|
-
this.emitter.emit("tool:end", {
|
|
240
|
-
toolName,
|
|
240
|
+
this.emitter.emit("tool:end", createToolEventPayload(toolName, {
|
|
241
241
|
responseTime: Date.now() - startTime,
|
|
242
242
|
success,
|
|
243
243
|
timestamp: Date.now(),
|
|
244
|
-
result
|
|
245
|
-
error: error ? error.message : undefined,
|
|
246
|
-
});
|
|
244
|
+
result,
|
|
245
|
+
error: error ? error.message : undefined,
|
|
246
|
+
}));
|
|
247
247
|
}
|
|
248
248
|
// Conversation memory support
|
|
249
249
|
conversationMemory;
|
|
@@ -4662,16 +4662,31 @@ Current user's request: ${currentInput}`;
|
|
|
4662
4662
|
};
|
|
4663
4663
|
const onToolStart = (...args) => {
|
|
4664
4664
|
const data = args[0];
|
|
4665
|
-
captureEvent("tool:start",
|
|
4665
|
+
captureEvent("tool:start", {
|
|
4666
|
+
...data,
|
|
4667
|
+
toolName: data.toolName ?? data.tool,
|
|
4668
|
+
});
|
|
4666
4669
|
};
|
|
4667
4670
|
const onToolEnd = (...args) => {
|
|
4668
4671
|
const data = args[0];
|
|
4669
|
-
|
|
4670
|
-
|
|
4672
|
+
const toolName = data.toolName ?? data.tool;
|
|
4673
|
+
const responseTime = data.responseTime ?? data.duration;
|
|
4674
|
+
const success = data.success ?? (data.error !== undefined ? false : undefined);
|
|
4675
|
+
const augmented = {
|
|
4676
|
+
...data,
|
|
4677
|
+
toolName,
|
|
4678
|
+
...(responseTime !== undefined ? { responseTime } : {}),
|
|
4679
|
+
...(success !== undefined ? { success } : {}),
|
|
4680
|
+
...(data.error !== undefined ? { error: data.error } : {}),
|
|
4681
|
+
};
|
|
4682
|
+
captureEvent("tool:end", augmented);
|
|
4683
|
+
if (augmented.result && augmented.result.uiComponent === true) {
|
|
4671
4684
|
captureEvent("ui-component", {
|
|
4672
|
-
toolName
|
|
4673
|
-
componentData:
|
|
4685
|
+
toolName,
|
|
4686
|
+
componentData: augmented.result,
|
|
4674
4687
|
timestamp: Date.now(),
|
|
4688
|
+
...(success !== undefined ? { success } : {}),
|
|
4689
|
+
...(responseTime !== undefined ? { responseTime } : {}),
|
|
4675
4690
|
});
|
|
4676
4691
|
}
|
|
4677
4692
|
};
|
|
@@ -5410,12 +5425,11 @@ Current user's request: ${currentInput}`;
|
|
|
5410
5425
|
this.activeToolExecutions.set(executionId, context);
|
|
5411
5426
|
this.currentStreamToolExecutions.push(context);
|
|
5412
5427
|
// Emit event (NeuroLinkEvents format for compatibility)
|
|
5413
|
-
this.emitter.emit("tool:start", {
|
|
5414
|
-
tool: toolName,
|
|
5428
|
+
this.emitter.emit("tool:start", createToolEventPayload(toolName, {
|
|
5415
5429
|
input,
|
|
5416
5430
|
timestamp: startTime,
|
|
5417
5431
|
executionId,
|
|
5418
|
-
});
|
|
5432
|
+
}));
|
|
5419
5433
|
logger.debug(`tool:start emitted for ${toolName}`, {
|
|
5420
5434
|
toolName,
|
|
5421
5435
|
executionId,
|
|
@@ -5473,14 +5487,15 @@ Current user's request: ${currentInput}`;
|
|
|
5473
5487
|
// Store in history
|
|
5474
5488
|
this.toolExecutionHistory.push(summary);
|
|
5475
5489
|
// Emit event (NeuroLinkEvents format for compatibility)
|
|
5476
|
-
this.emitter.emit("tool:end", {
|
|
5477
|
-
tool: toolName,
|
|
5490
|
+
this.emitter.emit("tool:end", createToolEventPayload(toolName, {
|
|
5478
5491
|
result,
|
|
5479
5492
|
error,
|
|
5493
|
+
success,
|
|
5494
|
+
responseTime: duration,
|
|
5480
5495
|
timestamp: endTime,
|
|
5481
5496
|
duration,
|
|
5482
5497
|
executionId: finalExecutionId,
|
|
5483
|
-
});
|
|
5498
|
+
}));
|
|
5484
5499
|
logger.debug(`tool:end emitted for ${toolName}`, {
|
|
5485
5500
|
toolName,
|
|
5486
5501
|
executionId: finalExecutionId,
|
|
@@ -6024,11 +6039,10 @@ Current user's request: ${currentInput}`;
|
|
|
6024
6039
|
options,
|
|
6025
6040
|
hasExternalManager: !!this.externalServerManager,
|
|
6026
6041
|
});
|
|
6027
|
-
this.emitter.emit("tool:start", {
|
|
6028
|
-
toolName,
|
|
6042
|
+
this.emitter.emit("tool:start", createToolEventPayload(toolName, {
|
|
6029
6043
|
timestamp: executionContext.executionStartTime,
|
|
6030
6044
|
input: params,
|
|
6031
|
-
});
|
|
6045
|
+
}));
|
|
6032
6046
|
const toolInfo = this.toolRegistry.getToolInfo(toolName);
|
|
6033
6047
|
const finalOptions = {
|
|
6034
6048
|
timeout: options?.timeout ??
|
|
@@ -38,6 +38,10 @@ export declare function sanitizeToolsForGemini(tools: Record<string, Tool>): {
|
|
|
38
38
|
tools: Record<string, Tool>;
|
|
39
39
|
dropped: string[];
|
|
40
40
|
};
|
|
41
|
+
export declare function normalizeToolsForJsonSchemaProvider(tools: Record<string, Tool>): {
|
|
42
|
+
tools: Record<string, Tool>;
|
|
43
|
+
normalized: string[];
|
|
44
|
+
};
|
|
41
45
|
/**
|
|
42
46
|
* Convert Vercel AI SDK tools to @google/genai FunctionDeclarations and an execute map.
|
|
43
47
|
*
|
|
@@ -12,7 +12,7 @@ import { randomUUID } from "node:crypto";
|
|
|
12
12
|
import { jsonSchema as aiJsonSchema, tool as createAISDKTool, } from "ai";
|
|
13
13
|
import { DEFAULT_MAX_STEPS, DEFAULT_TOOL_MAX_RETRIES, } from "../core/constants.js";
|
|
14
14
|
import { logger } from "../utils/logger.js";
|
|
15
|
-
import { convertZodToJsonSchema, inlineJsonSchema, isZodSchema, } from "../utils/schemaConversion.js";
|
|
15
|
+
import { convertZodToJsonSchema, inlineJsonSchema, isZodSchema, normalizeJsonSchemaObject, } from "../utils/schemaConversion.js";
|
|
16
16
|
import { createNativeThinkingConfig } from "../utils/thinkingConfig.js";
|
|
17
17
|
// ── Functions ──
|
|
18
18
|
/**
|
|
@@ -163,6 +163,44 @@ export function sanitizeToolsForGemini(tools) {
|
|
|
163
163
|
}
|
|
164
164
|
return { tools: sanitized, dropped };
|
|
165
165
|
}
|
|
166
|
+
export function normalizeToolsForJsonSchemaProvider(tools) {
|
|
167
|
+
const normalizedTools = {};
|
|
168
|
+
const normalized = [];
|
|
169
|
+
for (const [name, tool] of Object.entries(tools)) {
|
|
170
|
+
const legacyTool = tool;
|
|
171
|
+
const toolParams = legacyTool.parameters || tool.inputSchema;
|
|
172
|
+
let rawSchema;
|
|
173
|
+
if (isZodSchema(toolParams)) {
|
|
174
|
+
rawSchema = convertZodToJsonSchema(toolParams);
|
|
175
|
+
}
|
|
176
|
+
else if (toolParams && typeof toolParams === "object") {
|
|
177
|
+
rawSchema = toolParams;
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
rawSchema = { type: "object", properties: {} };
|
|
181
|
+
}
|
|
182
|
+
if (rawSchema.jsonSchema &&
|
|
183
|
+
typeof rawSchema.jsonSchema === "object" &&
|
|
184
|
+
!rawSchema.type) {
|
|
185
|
+
rawSchema = rawSchema.jsonSchema;
|
|
186
|
+
}
|
|
187
|
+
const schemaBefore = JSON.stringify(rawSchema);
|
|
188
|
+
const normalizedSchema = normalizeJsonSchemaObject(rawSchema);
|
|
189
|
+
if (JSON.stringify(normalizedSchema) !== schemaBefore) {
|
|
190
|
+
normalized.push(name);
|
|
191
|
+
}
|
|
192
|
+
const wrappedSchema = aiJsonSchema(normalizedSchema);
|
|
193
|
+
normalizedTools[name] = {
|
|
194
|
+
...tool,
|
|
195
|
+
inputSchema: wrappedSchema,
|
|
196
|
+
...(legacyTool.parameters ? { parameters: wrappedSchema } : {}),
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
return {
|
|
200
|
+
tools: normalizedTools,
|
|
201
|
+
normalized,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
166
204
|
/**
|
|
167
205
|
* Convert Vercel AI SDK tools to @google/genai FunctionDeclarations and an execute map.
|
|
168
206
|
*
|
|
@@ -23,7 +23,7 @@ import { convertZodToJsonSchema, inlineJsonSchema, } from "../utils/schemaConver
|
|
|
23
23
|
import { composeAbortSignals, createTimeoutController, TimeoutError, } from "../utils/timeout.js";
|
|
24
24
|
import { estimateTokens } from "../utils/tokenEstimation.js";
|
|
25
25
|
import { resolveToolChoice } from "../utils/toolChoice.js";
|
|
26
|
-
import { buildNativeConfig, buildNativeToolDeclarations, collectStreamChunks, collectStreamChunksIncremental, computeMaxSteps as computeMaxStepsShared, createTextChannel, executeNativeToolCalls, extractTextFromParts, handleMaxStepsTermination, pushModelResponseToHistory, sanitizeToolsForGemini, } from "./googleNativeGemini3.js";
|
|
26
|
+
import { buildNativeConfig, buildNativeToolDeclarations, collectStreamChunks, collectStreamChunksIncremental, computeMaxSteps as computeMaxStepsShared, createTextChannel, executeNativeToolCalls, extractTextFromParts, handleMaxStepsTermination, normalizeToolsForJsonSchemaProvider, pushModelResponseToHistory, sanitizeToolsForGemini, } from "./googleNativeGemini3.js";
|
|
27
27
|
import { getModelId } from "./providerTypeUtils.js";
|
|
28
28
|
// Import proper types for multimodal message handling
|
|
29
29
|
// Keep-alive note: Node.js native fetch and undici (used by createProxyFetch)
|
|
@@ -878,7 +878,15 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
878
878
|
Object.keys(sanitized.tools).length > 0 ? sanitized.tools : undefined;
|
|
879
879
|
}
|
|
880
880
|
else if (isAnthropic && Object.keys(rawTools).length > 0) {
|
|
881
|
-
|
|
881
|
+
const normalized = normalizeToolsForJsonSchemaProvider(rawTools);
|
|
882
|
+
if (normalized.normalized.length > 0) {
|
|
883
|
+
logger.debug("[GoogleVertex] Normalized Anthropic tool schema(s)", {
|
|
884
|
+
toolCount: normalized.normalized.length,
|
|
885
|
+
toolNames: normalized.normalized,
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
tools =
|
|
889
|
+
Object.keys(normalized.tools).length > 0 ? normalized.tools : undefined;
|
|
882
890
|
}
|
|
883
891
|
else {
|
|
884
892
|
tools = undefined;
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { jsonSchema, tool } from "ai";
|
|
12
12
|
import { randomBytes } from "crypto";
|
|
13
|
+
import { normalizeJsonSchemaObject } from "../utils/schemaConversion.js";
|
|
13
14
|
// ---------------------------------------------------------------------------
|
|
14
15
|
// Helpers
|
|
15
16
|
// ---------------------------------------------------------------------------
|
|
@@ -153,7 +154,7 @@ export function parseClaudeRequest(body) {
|
|
|
153
154
|
// Fallback providers consume AI SDK-style tools, not Claude wire-format
|
|
154
155
|
// tool descriptors. Wrap the raw JSON schema once here so every
|
|
155
156
|
// downstream provider sees a canonical `inputSchema` shape.
|
|
156
|
-
inputSchema: jsonSchema(t.input_schema ?? { type: "object" }),
|
|
157
|
+
inputSchema: jsonSchema(normalizeJsonSchemaObject(t.input_schema ?? { type: "object" })),
|
|
157
158
|
});
|
|
158
159
|
}
|
|
159
160
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ProxyHealthResponse, ProxyReadinessState } from "../types/index.js";
|
|
2
|
+
export type { ProxyHealthResponse, ProxyReadinessState };
|
|
3
|
+
export declare function createProxyReadinessState(startTimeMs?: number): ProxyReadinessState;
|
|
4
|
+
export declare function markProxyReady(state: ProxyReadinessState, readyAtMs?: number): void;
|
|
5
|
+
export declare function buildProxyHealthResponse(state: ProxyReadinessState, options: {
|
|
6
|
+
strategy: string;
|
|
7
|
+
passthrough: boolean;
|
|
8
|
+
version: string;
|
|
9
|
+
now?: number;
|
|
10
|
+
}): ProxyHealthResponse;
|
|
11
|
+
export declare function waitForProxyReadiness(args: {
|
|
12
|
+
host: string;
|
|
13
|
+
port: number;
|
|
14
|
+
timeoutMs?: number;
|
|
15
|
+
intervalMs?: number;
|
|
16
|
+
fetchImpl?: typeof fetch;
|
|
17
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export function createProxyReadinessState(startTimeMs = Date.now()) {
|
|
2
|
+
return {
|
|
3
|
+
startTimeMs,
|
|
4
|
+
acceptingConnections: false,
|
|
5
|
+
ready: false,
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
export function markProxyReady(state, readyAtMs = Date.now()) {
|
|
9
|
+
state.acceptingConnections = true;
|
|
10
|
+
state.ready = true;
|
|
11
|
+
state.readyAtMs = readyAtMs;
|
|
12
|
+
}
|
|
13
|
+
export function buildProxyHealthResponse(state, options) {
|
|
14
|
+
const now = options.now ?? Date.now();
|
|
15
|
+
return {
|
|
16
|
+
status: state.ready ? "ok" : "starting",
|
|
17
|
+
ready: state.ready,
|
|
18
|
+
acceptingConnections: state.acceptingConnections,
|
|
19
|
+
strategy: options.strategy,
|
|
20
|
+
passthrough: options.passthrough,
|
|
21
|
+
version: options.version,
|
|
22
|
+
startedAt: new Date(state.startTimeMs).toISOString(),
|
|
23
|
+
readyAt: state.readyAtMs ? new Date(state.readyAtMs).toISOString() : null,
|
|
24
|
+
uptime: Math.max(0, (now - state.startTimeMs) / 1000),
|
|
25
|
+
healthPath: "/health",
|
|
26
|
+
statusPath: "/status",
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function sleep(ms) {
|
|
30
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
31
|
+
}
|
|
32
|
+
export async function waitForProxyReadiness(args) {
|
|
33
|
+
const timeoutMs = args.timeoutMs ?? 5_000;
|
|
34
|
+
const intervalMs = args.intervalMs ?? 100;
|
|
35
|
+
const fetchImpl = args.fetchImpl ?? fetch;
|
|
36
|
+
const deadline = Date.now() + timeoutMs;
|
|
37
|
+
let lastError;
|
|
38
|
+
while (Date.now() < deadline) {
|
|
39
|
+
try {
|
|
40
|
+
const response = await fetchImpl(`http://${args.host}:${args.port}/health`, {
|
|
41
|
+
signal: AbortSignal.timeout(Math.min(intervalMs * 4, 1_000)),
|
|
42
|
+
});
|
|
43
|
+
if (response.ok) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
lastError = `health endpoint returned ${response.status}`;
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
lastError = error instanceof Error ? error.message : String(error);
|
|
50
|
+
}
|
|
51
|
+
await sleep(intervalMs);
|
|
52
|
+
}
|
|
53
|
+
throw new Error(`Proxy failed readiness check on http://${args.host}:${args.port}/health within ${timeoutMs}ms${lastError ? ` (${lastError})` : ""}`);
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=proxyHealth.js.map
|
|
@@ -489,12 +489,17 @@ export async function logBodyCapture(entry) {
|
|
|
489
489
|
: bridge.getCurrentTraceContext();
|
|
490
490
|
const redactedHeaders = redactHeaders(entry.headers);
|
|
491
491
|
const preparedBody = prepareRedactedBody(entry.body);
|
|
492
|
-
let stored
|
|
492
|
+
let stored;
|
|
493
493
|
try {
|
|
494
494
|
stored = await writeBodyArtifact(entry, redactedHeaders, preparedBody.value, preparedBody.truncated);
|
|
495
495
|
}
|
|
496
|
-
catch {
|
|
497
|
-
|
|
496
|
+
catch (writeError) {
|
|
497
|
+
logger.warn("[RequestLogger] writeBodyArtifact failed, falling back to in-memory body for OTLP", { error: writeError });
|
|
498
|
+
stored = {
|
|
499
|
+
redactedBody: preparedBody.value,
|
|
500
|
+
redactedBodyBytes: preparedBody.bytes,
|
|
501
|
+
bodyTruncated: preparedBody.truncated,
|
|
502
|
+
};
|
|
498
503
|
}
|
|
499
504
|
const dateStr = new Date(entry.timestamp).toISOString().split("T")[0];
|
|
500
505
|
const logFile = join(logDir, `proxy-debug-${dateStr}.jsonl`);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ClaudeProxyModelTier, ClaudeProxyRequestClass, ClaudeProxyRequestProfile, CooldownScope, CooldownSkippedAccount, FallbackEligibilityDecision, FallbackEntry, ParsedClaudeRequest, ProxyTranslationAttempt, ProxyTranslationPlan, RuntimeAccountState } from "../types/index.js";
|
|
2
|
+
export type { ClaudeProxyModelTier, ClaudeProxyRequestClass, ClaudeProxyRequestProfile, CooldownScope, CooldownSkippedAccount, FallbackEligibilityDecision, ProxyTranslationAttempt, ProxyTranslationPlan, };
|
|
3
|
+
export declare function inferClaudeProxyModelTier(modelName: string): ClaudeProxyModelTier;
|
|
4
|
+
export declare function classifyClaudeProxyRequest(requestedModel: string, parsed: ParsedClaudeRequest): ClaudeProxyRequestProfile;
|
|
5
|
+
export declare function getRequestClassCooldownKey(profile: ClaudeProxyRequestProfile): string;
|
|
6
|
+
export declare function getModelTierCooldownKey(profile: ClaudeProxyRequestProfile): string;
|
|
7
|
+
export declare function evaluateFallbackEligibility(profile: ClaudeProxyRequestProfile, candidate: {
|
|
8
|
+
provider?: string;
|
|
9
|
+
model?: string;
|
|
10
|
+
}): FallbackEligibilityDecision;
|
|
11
|
+
export declare function buildProxyTranslationPlan(primary: {
|
|
12
|
+
provider: string;
|
|
13
|
+
model?: string;
|
|
14
|
+
}, fallbackChain: FallbackEntry[], requestedModel: string, parsed: ParsedClaudeRequest): ProxyTranslationPlan;
|
|
15
|
+
export declare function summarizeSkippedFallbacks(plan: Pick<ProxyTranslationPlan, "profile" | "skipped">): string | null;
|
|
16
|
+
export declare function getActiveCooldownScope(state: RuntimeAccountState, profile: ClaudeProxyRequestProfile, now?: number): CooldownScope | null;
|
|
17
|
+
export declare function partitionAccountsByCooldown<T extends {
|
|
18
|
+
key: string;
|
|
19
|
+
}>(accounts: T[], getState: (account: T) => RuntimeAccountState, profile: ClaudeProxyRequestProfile, now?: number): {
|
|
20
|
+
eligible: T[];
|
|
21
|
+
skipped: CooldownSkippedAccount<T>[];
|
|
22
|
+
};
|
|
23
|
+
export declare function applyRateLimitCooldownScope(args: {
|
|
24
|
+
state: RuntimeAccountState;
|
|
25
|
+
profile: ClaudeProxyRequestProfile;
|
|
26
|
+
retryAfterMs?: number;
|
|
27
|
+
now?: number;
|
|
28
|
+
capMs: number;
|
|
29
|
+
}): {
|
|
30
|
+
backoffMs: number;
|
|
31
|
+
requestClassKey: string;
|
|
32
|
+
modelTierKey: string;
|
|
33
|
+
};
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
const STREAMING_CONVERSATIONAL_TOOL_THRESHOLD = 4;
|
|
2
|
+
const STRONG_TOOL_FIDELITY_THRESHOLD = 8;
|
|
3
|
+
const HIGH_TOOL_COUNT_THRESHOLD = 24;
|
|
4
|
+
const DEFAULT_COOLDOWN_FLOOR_MS = 1_000;
|
|
5
|
+
const HIGH_TOOL_COUNT_COOLDOWN_FLOOR_MS = 120_000;
|
|
6
|
+
const HIGH_FIDELITY_COOLDOWN_FLOOR_MS = 300_000;
|
|
7
|
+
export function inferClaudeProxyModelTier(modelName) {
|
|
8
|
+
const normalized = modelName.toLowerCase();
|
|
9
|
+
if (normalized.includes("opus")) {
|
|
10
|
+
return "opus";
|
|
11
|
+
}
|
|
12
|
+
if (normalized.includes("sonnet")) {
|
|
13
|
+
return "sonnet";
|
|
14
|
+
}
|
|
15
|
+
if (normalized.includes("haiku")) {
|
|
16
|
+
return "haiku";
|
|
17
|
+
}
|
|
18
|
+
return "other";
|
|
19
|
+
}
|
|
20
|
+
function detectToolHistory(parsed) {
|
|
21
|
+
return parsed.conversationMessages.some((message) => {
|
|
22
|
+
return (message.content.includes("[tool_use:") ||
|
|
23
|
+
message.content.includes("[tool_result:"));
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
export function classifyClaudeProxyRequest(requestedModel, parsed) {
|
|
27
|
+
const toolCount = Object.keys(parsed.tools).length;
|
|
28
|
+
const hasImages = parsed.images.length > 0;
|
|
29
|
+
const hasThinking = !!parsed.thinkingConfig?.enabled;
|
|
30
|
+
const hasToolHistory = detectToolHistory(parsed);
|
|
31
|
+
const requiresSpecificTool = !!parsed.toolChoiceName;
|
|
32
|
+
const requiresToolUse = parsed.toolChoice === "required" || requiresSpecificTool || hasToolHistory;
|
|
33
|
+
const requiresStrongToolFidelity = toolCount >= STRONG_TOOL_FIDELITY_THRESHOLD ||
|
|
34
|
+
requiresSpecificTool ||
|
|
35
|
+
hasToolHistory;
|
|
36
|
+
const isHighToolCountNonStream = !parsed.stream && toolCount >= HIGH_TOOL_COUNT_THRESHOLD;
|
|
37
|
+
const isStreamingConversational = parsed.stream &&
|
|
38
|
+
!hasImages &&
|
|
39
|
+
toolCount <= STREAMING_CONVERSATIONAL_TOOL_THRESHOLD &&
|
|
40
|
+
!requiresStrongToolFidelity;
|
|
41
|
+
const classes = [];
|
|
42
|
+
if (hasImages) {
|
|
43
|
+
classes.push("multimodal");
|
|
44
|
+
}
|
|
45
|
+
if (isHighToolCountNonStream) {
|
|
46
|
+
classes.push("high-tool-count-non-stream-structured");
|
|
47
|
+
}
|
|
48
|
+
if (requiresStrongToolFidelity) {
|
|
49
|
+
classes.push("strong-tool-fidelity");
|
|
50
|
+
}
|
|
51
|
+
if (isStreamingConversational) {
|
|
52
|
+
classes.push("streaming-conversational");
|
|
53
|
+
}
|
|
54
|
+
if (classes.length === 0) {
|
|
55
|
+
classes.push("standard");
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
requestedModel,
|
|
59
|
+
modelTier: inferClaudeProxyModelTier(requestedModel),
|
|
60
|
+
primaryClass: classes[0],
|
|
61
|
+
classes,
|
|
62
|
+
stream: parsed.stream,
|
|
63
|
+
toolCount,
|
|
64
|
+
hasImages,
|
|
65
|
+
hasThinking,
|
|
66
|
+
hasToolHistory,
|
|
67
|
+
requiresToolUse,
|
|
68
|
+
requiresSpecificTool,
|
|
69
|
+
requiresStrongToolFidelity,
|
|
70
|
+
isHighToolCountNonStream,
|
|
71
|
+
isStreamingConversational,
|
|
72
|
+
isMultimodal: hasImages,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
export function getRequestClassCooldownKey(profile) {
|
|
76
|
+
return `${profile.primaryClass}:${profile.requestedModel.toLowerCase()}`;
|
|
77
|
+
}
|
|
78
|
+
export function getModelTierCooldownKey(profile) {
|
|
79
|
+
return profile.modelTier;
|
|
80
|
+
}
|
|
81
|
+
function getQualityGuardReason(profile, provider, _model) {
|
|
82
|
+
// Only gate auto-provider fallback (no explicit provider).
|
|
83
|
+
// Configured fallback-chain entries are always allowed through —
|
|
84
|
+
// let them attempt the request and fail naturally if the provider
|
|
85
|
+
// cannot handle it.
|
|
86
|
+
if (!provider) {
|
|
87
|
+
if (profile.modelTier === "opus" ||
|
|
88
|
+
profile.requiresStrongToolFidelity ||
|
|
89
|
+
profile.isHighToolCountNonStream) {
|
|
90
|
+
return "auto-provider fallback is disabled for requests that require contract preservation";
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
export function evaluateFallbackEligibility(profile, candidate) {
|
|
97
|
+
const policyBlockReason = getQualityGuardReason(profile, candidate.provider, candidate.model);
|
|
98
|
+
if (policyBlockReason) {
|
|
99
|
+
return {
|
|
100
|
+
provider: candidate.provider,
|
|
101
|
+
model: candidate.model,
|
|
102
|
+
eligible: false,
|
|
103
|
+
reason: policyBlockReason,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
provider: candidate.provider,
|
|
108
|
+
model: candidate.model,
|
|
109
|
+
eligible: true,
|
|
110
|
+
reason: "eligible",
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
export function buildProxyTranslationPlan(primary, fallbackChain, requestedModel, parsed) {
|
|
114
|
+
const profile = classifyClaudeProxyRequest(requestedModel, parsed);
|
|
115
|
+
const attempts = [
|
|
116
|
+
{
|
|
117
|
+
provider: primary.provider,
|
|
118
|
+
model: primary.model,
|
|
119
|
+
label: `${primary.provider}/${primary.model ?? "unknown"}`,
|
|
120
|
+
},
|
|
121
|
+
];
|
|
122
|
+
const skipped = [];
|
|
123
|
+
for (const fallback of fallbackChain) {
|
|
124
|
+
if (fallback.provider === primary.provider &&
|
|
125
|
+
fallback.model === primary.model) {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
const decision = evaluateFallbackEligibility(profile, fallback);
|
|
129
|
+
if (!decision.eligible) {
|
|
130
|
+
skipped.push(decision);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
attempts.push({
|
|
134
|
+
provider: fallback.provider,
|
|
135
|
+
model: fallback.model,
|
|
136
|
+
label: `${fallback.provider}/${fallback.model}`,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
if (fallbackChain.length === 0) {
|
|
140
|
+
const autoDecision = evaluateFallbackEligibility(profile, {});
|
|
141
|
+
if (autoDecision.eligible) {
|
|
142
|
+
attempts.push({ label: "auto-provider" });
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
skipped.push(autoDecision);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
profile,
|
|
150
|
+
attempts,
|
|
151
|
+
skipped,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
export function summarizeSkippedFallbacks(plan) {
|
|
155
|
+
if (plan.skipped.length === 0) {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
const summary = plan.skipped
|
|
159
|
+
.map((decision) => {
|
|
160
|
+
const label = decision.provider
|
|
161
|
+
? `${decision.provider}/${decision.model ?? "unknown"}`
|
|
162
|
+
: "auto-provider";
|
|
163
|
+
return `${label}: ${decision.reason}`;
|
|
164
|
+
})
|
|
165
|
+
.join("; ");
|
|
166
|
+
return `Fallback policy preserved the requested ${plan.profile.primaryClass} contract by skipping ineligible targets. ${summary}`;
|
|
167
|
+
}
|
|
168
|
+
export function getActiveCooldownScope(state, profile, now = Date.now()) {
|
|
169
|
+
let longest = null;
|
|
170
|
+
const requestClassKey = getRequestClassCooldownKey(profile);
|
|
171
|
+
const requestClassUntil = state.requestClassCooldowns?.[requestClassKey] ?? undefined;
|
|
172
|
+
if (requestClassUntil && requestClassUntil > now) {
|
|
173
|
+
longest = {
|
|
174
|
+
scope: "request_class",
|
|
175
|
+
key: requestClassKey,
|
|
176
|
+
until: requestClassUntil,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
const modelTierKey = getModelTierCooldownKey(profile);
|
|
180
|
+
const modelTierUntil = state.modelTierCooldowns?.[modelTierKey] ?? undefined;
|
|
181
|
+
if (modelTierUntil &&
|
|
182
|
+
modelTierUntil > now &&
|
|
183
|
+
modelTierUntil > (longest?.until ?? 0)) {
|
|
184
|
+
longest = {
|
|
185
|
+
scope: "model_tier",
|
|
186
|
+
key: modelTierKey,
|
|
187
|
+
until: modelTierUntil,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
if (state.coolingUntil &&
|
|
191
|
+
state.coolingUntil > now &&
|
|
192
|
+
state.coolingUntil > (longest?.until ?? 0)) {
|
|
193
|
+
longest = {
|
|
194
|
+
scope: "generic",
|
|
195
|
+
key: "generic",
|
|
196
|
+
until: state.coolingUntil,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
return longest;
|
|
200
|
+
}
|
|
201
|
+
export function partitionAccountsByCooldown(accounts, getState, profile, now = Date.now()) {
|
|
202
|
+
const eligible = [];
|
|
203
|
+
const skipped = [];
|
|
204
|
+
for (const account of accounts) {
|
|
205
|
+
const cooldown = getActiveCooldownScope(getState(account), profile, now);
|
|
206
|
+
if (cooldown) {
|
|
207
|
+
skipped.push({ account, cooldown });
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
eligible.push(account);
|
|
211
|
+
}
|
|
212
|
+
return {
|
|
213
|
+
eligible,
|
|
214
|
+
skipped,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
export function applyRateLimitCooldownScope(args) {
|
|
218
|
+
const now = args.now ?? Date.now();
|
|
219
|
+
const requestClassKey = getRequestClassCooldownKey(args.profile);
|
|
220
|
+
const modelTierKey = getModelTierCooldownKey(args.profile);
|
|
221
|
+
const rcBackoffLevels = args.state.requestClassBackoffLevels ?? {};
|
|
222
|
+
const mtBackoffLevels = args.state.modelTierBackoffLevels ?? {};
|
|
223
|
+
const scopedBackoffLevel = Math.max(rcBackoffLevels[requestClassKey] ?? 0, mtBackoffLevels[modelTierKey] ?? 0);
|
|
224
|
+
const floorMs = args.profile.modelTier === "opus" || args.profile.requiresStrongToolFidelity
|
|
225
|
+
? HIGH_FIDELITY_COOLDOWN_FLOOR_MS
|
|
226
|
+
: args.profile.isHighToolCountNonStream
|
|
227
|
+
? HIGH_TOOL_COUNT_COOLDOWN_FLOOR_MS
|
|
228
|
+
: DEFAULT_COOLDOWN_FLOOR_MS;
|
|
229
|
+
const baseCooldownMs = Math.max(args.retryAfterMs ?? 0, floorMs);
|
|
230
|
+
const backoffMs = Math.min(baseCooldownMs * 2 ** scopedBackoffLevel, args.capMs);
|
|
231
|
+
const until = now + backoffMs;
|
|
232
|
+
args.state.requestClassCooldowns = {
|
|
233
|
+
...(args.state.requestClassCooldowns ?? {}),
|
|
234
|
+
[requestClassKey]: Math.max(args.state.requestClassCooldowns?.[requestClassKey] ?? 0, until),
|
|
235
|
+
};
|
|
236
|
+
args.state.modelTierCooldowns = {
|
|
237
|
+
...(args.state.modelTierCooldowns ?? {}),
|
|
238
|
+
[modelTierKey]: Math.max(args.state.modelTierCooldowns?.[modelTierKey] ?? 0, until),
|
|
239
|
+
};
|
|
240
|
+
args.state.requestClassBackoffLevels = {
|
|
241
|
+
...rcBackoffLevels,
|
|
242
|
+
[requestClassKey]: (rcBackoffLevels[requestClassKey] ?? 0) + 1,
|
|
243
|
+
};
|
|
244
|
+
args.state.modelTierBackoffLevels = {
|
|
245
|
+
...mtBackoffLevels,
|
|
246
|
+
[modelTierKey]: (mtBackoffLevels[modelTierKey] ?? 0) + 1,
|
|
247
|
+
};
|
|
248
|
+
args.state.backoffLevel += 1;
|
|
249
|
+
return {
|
|
250
|
+
backoffMs,
|
|
251
|
+
requestClassKey,
|
|
252
|
+
modelTierKey,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
//# sourceMappingURL=routingPolicy.js.map
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { mkdir, rename, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { basename, dirname, join } from "node:path";
|
|
4
|
+
const writeLocks = new Map();
|
|
5
|
+
async function writeSnapshotFile(targetPath, payload, mode) {
|
|
6
|
+
const dir = dirname(targetPath);
|
|
7
|
+
const baseName = basename(targetPath);
|
|
8
|
+
await mkdir(dir, { recursive: true });
|
|
9
|
+
const tempPath = join(dir, `.${baseName}.${process.pid}.${randomUUID()}.tmp`);
|
|
10
|
+
try {
|
|
11
|
+
await writeFile(tempPath, payload, { mode });
|
|
12
|
+
await rename(tempPath, targetPath);
|
|
13
|
+
}
|
|
14
|
+
finally {
|
|
15
|
+
await rm(tempPath, { force: true }).catch(() => {
|
|
16
|
+
// Best-effort cleanup only.
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export async function writeJsonSnapshotAtomically(targetPath, data, mode = 0o600) {
|
|
21
|
+
const payload = JSON.stringify(data, null, 2);
|
|
22
|
+
const previous = writeLocks.get(targetPath) ?? Promise.resolve();
|
|
23
|
+
const next = previous
|
|
24
|
+
.catch(() => {
|
|
25
|
+
// Preserve the queue even if a previous write failed.
|
|
26
|
+
})
|
|
27
|
+
.then(() => writeSnapshotFile(targetPath, payload, mode));
|
|
28
|
+
writeLocks.set(targetPath, next);
|
|
29
|
+
try {
|
|
30
|
+
await next;
|
|
31
|
+
}
|
|
32
|
+
finally {
|
|
33
|
+
if (writeLocks.get(targetPath) === next) {
|
|
34
|
+
writeLocks.delete(targetPath);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export function clearSnapshotWriteLocksForTests() {
|
|
39
|
+
writeLocks.clear();
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=snapshotPersistence.js.map
|