@juspay/neurolink 9.42.1 → 9.44.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/browser/neurolink.min.js +300 -300
  3. package/dist/cli/commands/mcp.js +15 -3
  4. package/dist/cli/commands/proxy.js +29 -6
  5. package/dist/core/baseProvider.js +12 -3
  6. package/dist/core/factory.js +4 -4
  7. package/dist/core/modules/ToolsManager.d.ts +1 -0
  8. package/dist/core/modules/ToolsManager.js +40 -42
  9. package/dist/core/toolEvents.d.ts +3 -0
  10. package/dist/core/toolEvents.js +7 -0
  11. package/dist/evaluation/scorers/scorerRegistry.js +3 -2
  12. package/dist/lib/core/baseProvider.js +12 -3
  13. package/dist/lib/core/factory.js +4 -4
  14. package/dist/lib/core/modules/ToolsManager.d.ts +1 -0
  15. package/dist/lib/core/modules/ToolsManager.js +40 -42
  16. package/dist/lib/core/toolEvents.d.ts +3 -0
  17. package/dist/lib/core/toolEvents.js +8 -0
  18. package/dist/lib/evaluation/scorers/scorerRegistry.js +3 -2
  19. package/dist/lib/neurolink.js +33 -19
  20. package/dist/lib/providers/googleNativeGemini3.d.ts +4 -0
  21. package/dist/lib/providers/googleNativeGemini3.js +39 -1
  22. package/dist/lib/providers/googleVertex.js +10 -2
  23. package/dist/lib/proxy/claudeFormat.js +2 -1
  24. package/dist/lib/proxy/proxyHealth.d.ts +17 -0
  25. package/dist/lib/proxy/proxyHealth.js +55 -0
  26. package/dist/lib/proxy/requestLogger.js +8 -3
  27. package/dist/lib/proxy/routingPolicy.d.ts +33 -0
  28. package/dist/lib/proxy/routingPolicy.js +255 -0
  29. package/dist/lib/proxy/snapshotPersistence.d.ts +2 -0
  30. package/dist/lib/proxy/snapshotPersistence.js +41 -0
  31. package/dist/lib/server/routes/claudeProxyRoutes.d.ts +1 -9
  32. package/dist/lib/server/routes/claudeProxyRoutes.js +304 -219
  33. package/dist/lib/tasks/store/redisTaskStore.js +34 -16
  34. package/dist/lib/types/cli.d.ts +4 -0
  35. package/dist/lib/types/proxyTypes.d.ts +87 -0
  36. package/dist/lib/types/tools.d.ts +18 -0
  37. package/dist/lib/utils/schemaConversion.d.ts +1 -0
  38. package/dist/lib/utils/schemaConversion.js +3 -0
  39. package/dist/neurolink.js +33 -19
  40. package/dist/providers/googleNativeGemini3.d.ts +4 -0
  41. package/dist/providers/googleNativeGemini3.js +39 -1
  42. package/dist/providers/googleVertex.js +10 -2
  43. package/dist/proxy/claudeFormat.js +2 -1
  44. package/dist/proxy/proxyHealth.d.ts +17 -0
  45. package/dist/proxy/proxyHealth.js +54 -0
  46. package/dist/proxy/requestLogger.js +8 -3
  47. package/dist/proxy/routingPolicy.d.ts +33 -0
  48. package/dist/proxy/routingPolicy.js +254 -0
  49. package/dist/proxy/snapshotPersistence.d.ts +2 -0
  50. package/dist/proxy/snapshotPersistence.js +40 -0
  51. package/dist/server/routes/claudeProxyRoutes.d.ts +1 -9
  52. package/dist/server/routes/claudeProxyRoutes.js +304 -219
  53. package/dist/tasks/store/redisTaskStore.js +34 -16
  54. package/dist/types/cli.d.ts +4 -0
  55. package/dist/types/proxyTypes.d.ts +87 -0
  56. package/dist/types/tools.d.ts +18 -0
  57. package/dist/utils/schemaConversion.d.ts +1 -0
  58. package/dist/utils/schemaConversion.js +3 -0
  59. package/package.json +1 -1
@@ -183,22 +183,40 @@ export class RedisTaskStore {
183
183
  const ttlSeconds = Math.ceil(ttlMs / 1000);
184
184
  // Set TTL on associated keys best-effort. A successful task write should not
185
185
  // be surfaced as a failure just because the retention metadata could not be updated.
186
- client.expire(taskRunsKey(task.id), ttlSeconds).catch((err) => {
187
- logger.warn("[TaskStore:Redis] Failed to set TTL on task runs key — task data may outlive retention window", {
188
- taskId: task.id,
189
- key: taskRunsKey(task.id),
190
- ttlSeconds,
191
- error: String(err),
192
- });
193
- });
194
- client.expire(taskHistoryKey(task.id), ttlSeconds).catch((err) => {
195
- logger.warn("[TaskStore:Redis] Failed to set TTL on task history key — task data may outlive retention window", {
196
- taskId: task.id,
197
- key: taskHistoryKey(task.id),
198
- ttlSeconds,
199
- error: String(err),
200
- });
201
- });
186
+ void (async () => {
187
+ const runsKey = taskRunsKey(task.id);
188
+ for (let attempt = 1; attempt <= 3; attempt++) {
189
+ try {
190
+ await client.expire(runsKey, ttlSeconds);
191
+ break;
192
+ }
193
+ catch (err) {
194
+ if (attempt === 3) {
195
+ logger.warn("[TaskStore:Redis] expire failed after 3 attempts on task runs key — task data may outlive retention window", { taskId: task.id, key: runsKey, ttlSeconds, err: String(err) });
196
+ }
197
+ else {
198
+ await new Promise((r) => setTimeout(r, 100 * attempt));
199
+ }
200
+ }
201
+ }
202
+ })();
203
+ void (async () => {
204
+ const histKey = taskHistoryKey(task.id);
205
+ for (let attempt = 1; attempt <= 3; attempt++) {
206
+ try {
207
+ await client.expire(histKey, ttlSeconds);
208
+ break;
209
+ }
210
+ catch (err) {
211
+ if (attempt === 3) {
212
+ logger.warn("[TaskStore:Redis] expire failed after 3 attempts on task history key — task data may outlive retention window", { taskId: task.id, key: histKey, ttlSeconds, err: String(err) });
213
+ }
214
+ else {
215
+ await new Promise((r) => setTimeout(r, 100 * attempt));
216
+ }
217
+ }
218
+ }
219
+ })();
202
220
  }
203
221
  }
204
222
  //# sourceMappingURL=redisTaskStore.js.map
@@ -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[];
@@ -548,6 +548,7 @@ export type AnthropicAuthRetryResult = {
548
548
  export type AnthropicNonOkResult = {
549
549
  response?: Response | unknown;
550
550
  continueLoop: boolean;
551
+ retrySameAccount?: boolean;
551
552
  lastError: unknown;
552
553
  authFailureMessage: string | null;
553
554
  sawTransientFailure: boolean;
@@ -570,6 +571,7 @@ export type PreparedAnthropicAccountAttempt = {
570
571
  };
571
572
  export type AnthropicUpstreamFetchResult = {
572
573
  continueLoop: boolean;
574
+ retrySameAccount?: boolean;
573
575
  response?: Response;
574
576
  lastError: unknown;
575
577
  sawRateLimit: boolean;
@@ -639,6 +641,10 @@ export type RuntimeAccountState = {
639
641
  backoffLevel: number;
640
642
  consecutiveRefreshFailures: number;
641
643
  permanentlyDisabled: boolean;
644
+ requestClassCooldowns?: Record<string, number>;
645
+ modelTierCooldowns?: Record<string, number>;
646
+ requestClassBackoffLevels?: Record<string, number>;
647
+ modelTierBackoffLevels?: Record<string, number>;
642
648
  lastToken?: string;
643
649
  lastRefreshToken?: string;
644
650
  };
@@ -689,3 +695,84 @@ export type CachedSession = {
689
695
  userId: string;
690
696
  expiresAt: number;
691
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
  */
@@ -20,6 +20,7 @@ export declare function inlineJsonSchema(schema: Record<string, unknown>, defini
20
20
  * 3. Plain JSON Schema objects (have `type`/`properties` but no `_def`) — returned as-is
21
21
  */
22
22
  export declare function convertZodToJsonSchema(zodSchema: ZodUnknownSchema): object;
23
+ export declare function normalizeJsonSchemaObject(schema: Record<string, unknown> | undefined | null): Record<string, unknown>;
23
24
  /**
24
25
  * Check if a value is a Zod schema
25
26
  */
@@ -138,6 +138,9 @@ export function convertZodToJsonSchema(zodSchema) {
138
138
  return { type: "object", properties: {} };
139
139
  }
140
140
  }
141
+ export function normalizeJsonSchemaObject(schema) {
142
+ return ensureTypeField(inlineJsonSchema(schema ? { ...schema } : { type: "object", properties: {} }));
143
+ }
141
144
  /**
142
145
  * Ensure a JSON Schema object has a `type` field (required by Vertex/Gemini).
143
146
  */
package/dist/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: result, // Enhanced: include actual result
245
- error: error ? error.message : undefined, // Emit as string, not Error object
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", data);
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
- captureEvent("tool:end", data);
4670
- if (data.result && data.result.uiComponent === true) {
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: data.toolName,
4673
- componentData: data.result,
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
- tools = rawTools;
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,54 @@
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
+ }
@@ -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
- // Best-effort artifact persistence; continue with in-memory metadata only.
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
+ };