@juspay/neurolink 9.75.0 → 9.77.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 (51) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/browser/neurolink.min.js +341 -341
  3. package/dist/core/baseProvider.js +29 -4
  4. package/dist/core/toolDedup.d.ts +51 -0
  5. package/dist/core/toolDedup.js +193 -0
  6. package/dist/index.d.ts +31 -0
  7. package/dist/index.js +35 -0
  8. package/dist/lib/core/baseProvider.js +29 -4
  9. package/dist/lib/core/toolDedup.d.ts +51 -0
  10. package/dist/lib/core/toolDedup.js +194 -0
  11. package/dist/lib/index.d.ts +31 -0
  12. package/dist/lib/index.js +35 -0
  13. package/dist/lib/neurolink.d.ts +36 -1
  14. package/dist/lib/neurolink.js +387 -90
  15. package/dist/lib/routing/index.d.ts +7 -0
  16. package/dist/lib/routing/index.js +8 -0
  17. package/dist/lib/routing/modelPool.d.ts +83 -0
  18. package/dist/lib/routing/modelPool.js +243 -0
  19. package/dist/lib/routing/requestRouter.d.ts +30 -0
  20. package/dist/lib/routing/requestRouter.js +81 -0
  21. package/dist/lib/types/config.d.ts +32 -2
  22. package/dist/lib/types/index.d.ts +3 -0
  23. package/dist/lib/types/index.js +5 -0
  24. package/dist/lib/types/modelPool.d.ts +47 -0
  25. package/dist/lib/types/modelPool.js +11 -0
  26. package/dist/lib/types/requestRouter.d.ts +75 -0
  27. package/dist/lib/types/requestRouter.js +16 -0
  28. package/dist/lib/types/toolDedup.d.ts +45 -0
  29. package/dist/lib/types/toolDedup.js +14 -0
  30. package/dist/lib/utils/providerErrorClassification.d.ts +24 -0
  31. package/dist/lib/utils/providerErrorClassification.js +89 -0
  32. package/dist/neurolink.d.ts +36 -1
  33. package/dist/neurolink.js +387 -90
  34. package/dist/routing/index.d.ts +7 -0
  35. package/dist/routing/index.js +7 -0
  36. package/dist/routing/modelPool.d.ts +83 -0
  37. package/dist/routing/modelPool.js +242 -0
  38. package/dist/routing/requestRouter.d.ts +30 -0
  39. package/dist/routing/requestRouter.js +80 -0
  40. package/dist/types/config.d.ts +32 -2
  41. package/dist/types/index.d.ts +3 -0
  42. package/dist/types/index.js +5 -0
  43. package/dist/types/modelPool.d.ts +47 -0
  44. package/dist/types/modelPool.js +10 -0
  45. package/dist/types/requestRouter.d.ts +75 -0
  46. package/dist/types/requestRouter.js +15 -0
  47. package/dist/types/toolDedup.d.ts +45 -0
  48. package/dist/types/toolDedup.js +13 -0
  49. package/dist/utils/providerErrorClassification.d.ts +24 -0
  50. package/dist/utils/providerErrorClassification.js +88 -0
  51. package/package.json +6 -2
@@ -0,0 +1,194 @@
1
+ /**
2
+ * Tool-signature deduplication — pure module, no I/O, no side effects.
3
+ *
4
+ * ## Algorithm
5
+ *
6
+ * `computeToolSignature` builds a canonical, order-insensitive string from:
7
+ * 1. The tool's name (exact, case-preserved).
8
+ * 2. A normalised description: lowercased, all whitespace runs collapsed to a
9
+ * single space, leading/trailing whitespace stripped.
10
+ * 3. A sorted, deduplicated list of JSON-schema property names (top-level
11
+ * `properties` keys from `inputSchema`). If the schema exposes a `type`
12
+ * field on each property, that type string is appended as `name:type`.
13
+ *
14
+ * `dedupeTools` clusters tools by pairwise token-set Jaccard similarity:
15
+ *
16
+ * Jaccard(A, B) = |A ∩ B| / |A ∪ B|
17
+ *
18
+ * where A and B are the _sets_ of whitespace-split tokens from each tool's
19
+ * canonical signature. This is deterministic, symmetric, and independent of
20
+ * token ordering — making it robust to minor rewording while being fast (O(n²)
21
+ * in the number of tools, which is bounded in practice by provider limits).
22
+ *
23
+ * The first tool encountered in stable input-iteration order becomes the
24
+ * cluster representative; subsequent tools in the same cluster are dropped.
25
+ */
26
+ // ---------------------------------------------------------------------------
27
+ // Internal helpers
28
+ // ---------------------------------------------------------------------------
29
+ /**
30
+ * Extract top-level property names (and optionally their types) from a JSON
31
+ * Schema-shaped `inputSchema`. Returns a sorted, stable list.
32
+ *
33
+ * The `inputSchema` on a `Tool` is typed as `FlexibleSchema<INPUT>` (opaque at
34
+ * runtime), so we access it via index to avoid casting.
35
+ */
36
+ function extractSchemaTokens(inputSchema) {
37
+ if (!inputSchema) {
38
+ return [];
39
+ }
40
+ // FlexibleSchema may be a Zod schema, a jsonSchema() wrapper, or a plain
41
+ // JSON Schema object. All three expose `jsonSchema` or fall through to the
42
+ // raw object. We look for the plain JSON Schema `properties` key.
43
+ const schema = inputSchema;
44
+ // Vercel AI SDK wraps plain schemas in { jsonSchema: {...}, ... }
45
+ const rawSchema = schema["jsonSchema"] !== undefined
46
+ ? schema["jsonSchema"]
47
+ : schema;
48
+ const properties = rawSchema["properties"];
49
+ if (!properties || typeof properties !== "object" || properties === null) {
50
+ return [];
51
+ }
52
+ const tokens = [];
53
+ const props = properties;
54
+ for (const propName of Object.keys(props)) {
55
+ const propDef = props[propName];
56
+ if (propDef && typeof propDef === "object" && propDef !== null) {
57
+ const t = propDef["type"];
58
+ tokens.push(typeof t === "string" ? `${propName}:${t}` : propName);
59
+ }
60
+ else {
61
+ tokens.push(propName);
62
+ }
63
+ }
64
+ return tokens.sort();
65
+ }
66
+ /**
67
+ * Normalise a description string: lowercase, collapse whitespace, strip ends.
68
+ */
69
+ function normaliseDescription(desc) {
70
+ if (!desc) {
71
+ return "";
72
+ }
73
+ return desc.toLowerCase().replace(/\s+/g, " ").trim();
74
+ }
75
+ // ---------------------------------------------------------------------------
76
+ // Public API
77
+ // ---------------------------------------------------------------------------
78
+ /**
79
+ * Build a canonical, order-insensitive signature string for a named tool.
80
+ *
81
+ * The signature is composed of:
82
+ * - the tool's name
83
+ * - a normalised description (lowercased, whitespace-collapsed)
84
+ * - sorted parameter property names (with types when available)
85
+ *
86
+ * Stable regardless of property declaration order in the schema.
87
+ */
88
+ export function computeToolSignature(name, tool) {
89
+ const descPart = normaliseDescription(tool.description);
90
+ const schemaParts = extractSchemaTokens(tool.inputSchema);
91
+ // Pipe-separated sections keep tokens within each section unambiguous.
92
+ return [name, descPart, schemaParts.join(",")].join("|");
93
+ }
94
+ /**
95
+ * Compute the token-set Jaccard similarity between two signature strings.
96
+ *
97
+ * Jaccard(A, B) = |A ∩ B| / |A ∪ B|
98
+ *
99
+ * Splits on whitespace and also on the pipe/comma delimiters used by
100
+ * `computeToolSignature` so that structural differences (e.g. a parameter
101
+ * missing from one tool) are properly reflected.
102
+ */
103
+ function jaccardSimilarity(sigA, sigB) {
104
+ const tokenise = (s) => new Set(s.split(/[\s|,]+/).filter((t) => t.length > 0));
105
+ const setA = tokenise(sigA);
106
+ const setB = tokenise(sigB);
107
+ if (setA.size === 0 && setB.size === 0) {
108
+ return 1;
109
+ }
110
+ if (setA.size === 0 || setB.size === 0) {
111
+ return 0;
112
+ }
113
+ let intersection = 0;
114
+ for (const token of setA) {
115
+ if (setB.has(token)) {
116
+ intersection += 1;
117
+ }
118
+ }
119
+ const union = setA.size + setB.size - intersection;
120
+ return intersection / union;
121
+ }
122
+ /**
123
+ * Collapse near-duplicate tools in a name→Tool record.
124
+ *
125
+ * When `options.enabled` is falsy (the default), returns the original record
126
+ * and an empty `removed` array — byte-for-byte unchanged behaviour.
127
+ *
128
+ * When enabled, tools whose token-set Jaccard similarity (over their canonical
129
+ * signatures) meets or exceeds `options.threshold` (default 0.9) are collapsed
130
+ * to a single representative (the first in stable iteration order).
131
+ *
132
+ * Any exception thrown internally returns the ORIGINAL tool set (fail-open).
133
+ */
134
+ export function dedupeTools(tools, options) {
135
+ const noOp = { tools, removed: [] };
136
+ if (!options.enabled) {
137
+ return noOp;
138
+ }
139
+ try {
140
+ const rawThreshold = options.threshold ?? 0.9;
141
+ const threshold = Math.min(1, Math.max(0, rawThreshold));
142
+ const entries = Object.entries(tools);
143
+ if (entries.length <= 1) {
144
+ return noOp;
145
+ }
146
+ // Pre-compute signatures once.
147
+ const signatures = new Map();
148
+ for (const [name, tool] of entries) {
149
+ signatures.set(name, computeToolSignature(name, tool));
150
+ }
151
+ // representative[toolName] = name of the cluster representative.
152
+ // Tools not yet assigned to a cluster act as their own representative.
153
+ const representativeOf = new Map();
154
+ for (const [nameA] of entries) {
155
+ if (representativeOf.has(nameA)) {
156
+ // Already absorbed into another cluster.
157
+ continue;
158
+ }
159
+ const sigA = signatures.get(nameA) ?? "";
160
+ for (const [nameB] of entries) {
161
+ if (nameA === nameB || representativeOf.has(nameB)) {
162
+ continue;
163
+ }
164
+ const sigB = signatures.get(nameB) ?? "";
165
+ const sim = jaccardSimilarity(sigA, sigB);
166
+ if (sim >= threshold) {
167
+ representativeOf.set(nameB, nameA);
168
+ }
169
+ }
170
+ }
171
+ if (representativeOf.size === 0) {
172
+ return noOp;
173
+ }
174
+ const dedupedTools = {};
175
+ const removed = [];
176
+ for (const [name, tool] of entries) {
177
+ const rep = representativeOf.get(name);
178
+ if (rep !== undefined) {
179
+ // Compute similarity again to include in the removed record.
180
+ const sim = jaccardSimilarity(signatures.get(name) ?? "", signatures.get(rep) ?? "");
181
+ removed.push({ name, duplicateOf: rep, similarity: sim });
182
+ }
183
+ else {
184
+ dedupedTools[name] = tool;
185
+ }
186
+ }
187
+ return { tools: dedupedTools, removed };
188
+ }
189
+ catch {
190
+ // Fail open — return the original set.
191
+ return noOp;
192
+ }
193
+ }
194
+ //# sourceMappingURL=toolDedup.js.map
@@ -214,6 +214,7 @@ export declare function createBestAIProvider(requestedProvider?: string, modelNa
214
214
  * ```
215
215
  */
216
216
  export { CircuitBreakerManager, calculateExpiresAt, createMCPServer, createOAuthProviderFromConfig, DEFAULT_HTTP_RETRY_CONFIG, DEFAULT_RATE_LIMIT_CONFIG, executeMCP, FileTokenStorage, getMCPStats, getServerInfo, globalCircuitBreakerManager, globalRateLimiterManager, HTTPRateLimiter, InMemoryTokenStorage, initializeMCPEcosystem, isRetryableHTTPError, isRetryableStatusCode, isTokenExpired, listMCPs, CircuitBreakerOpenError, MCPCircuitBreaker, mcpLogger, NeuroLinkOAuthProvider, RateLimiterManager, validateServerTools, validateTool as validateMCPTool, withHTTPRetry, MCPToolRegistry, ExternalServerManager, MCPClientFactory, ToolRouter, createToolRouter, DEFAULT_ROUTER_CONFIG, ToolCache, createToolCache, DEFAULT_CACHE_CONFIG, ToolResultCache, createToolResultCache, RequestBatcher, ToolCallBatcher, createRequestBatcher, createToolCallBatcher, DEFAULT_BATCH_CONFIG, inferAnnotations, createAnnotatedTool, validateAnnotations, filterToolsByAnnotations, mergeAnnotations, getAnnotationSummary, requiresConfirmation, isSafeToRetry, getToolSafetyLevel, ElicitationManager, globalElicitationManager, EnhancedToolDiscovery, MCPRegistryClient, globalMCPRegistryClient, getWellKnownServer, getAllWellKnownServers, MCPServerBase, MultiServerManager, globalMultiServerManager, AgentExposureManager, exposeAgentAsTool, exposeAgentsAsTools, exposeWorkflowAsTool, exposeWorkflowsAsTools, globalAgentExposureManager, ServerCapabilitiesManager, createTextResource, createJsonResource, createPrompt, neuroLinkToolToMCP, mcpToolToNeuroLink, batchConvertToMCP, batchConvertToNeuroLink, sanitizeToolName, validateToolName, createToolFromFunction, mcpProtocolToolToServerTool, serverToolToMCPProtocol, TOOL_COMPATIBILITY, ToolIntegrationManager, globalToolIntegrationManager, wrapToolWithElicitation, wrapToolsWithElicitation, createElicitationContext, confirmationMiddleware, validationMiddleware, loggingMiddleware, createRetryMiddleware, createTimeoutMiddleware, createToolMiddlewareChain, ElicitationProtocolAdapter, globalElicitationProtocol, isElicitationProtocolMessage, protocolMessageToElicitation, elicitationResponseToProtocol, createElicitationRequest, createElicitationResponse, createElicitationCancel, createTextInputRequest, createSelectRequest, createConfirmationRequest, createFormRequest, } from "./mcp/index.js";
217
+ export { computeToolSignature, dedupeTools } from "./core/toolDedup.js";
217
218
  export { logger } from "./utils/logger.js";
218
219
  export { getPoolStats } from "./utils/redis.js";
219
220
  export { ToolRoutingCache } from "./core/toolRoutingCache.js";
@@ -336,6 +337,36 @@ export { BALANCED_ADAPTIVE_WORKFLOW, createAdaptiveWorkflow, QUALITY_MAX_WORKFLO
336
337
  export { CONSENSUS_3_FAST_WORKFLOW, CONSENSUS_3_WORKFLOW, createConsensus3WithPrompt, } from "./workflow/workflows/consensusWorkflow.js";
337
338
  export { AGGRESSIVE_FALLBACK_WORKFLOW, FAST_FALLBACK_WORKFLOW, } from "./workflow/workflows/fallbackWorkflow.js";
338
339
  export { createMultiJudgeWorkflow, MULTI_JUDGE_3_WORKFLOW, MULTI_JUDGE_5_WORKFLOW, } from "./workflow/workflows/multiJudgeWorkflow.js";
340
+ /**
341
+ * ModelPool and RequestRouter — opt-in multi-provider failover with
342
+ * error-class-aware cooldown, and a pluggable pre-call provider/model router.
343
+ *
344
+ * @example ModelPool
345
+ * ```typescript
346
+ * import { ModelPool, classifyProviderError } from '@juspay/neurolink';
347
+ *
348
+ * const pool = new ModelPool({
349
+ * members: [
350
+ * { provider: 'anthropic', model: 'claude-sonnet-4-5' },
351
+ * { provider: 'vertex', model: 'gemini-2.5-flash' },
352
+ * ],
353
+ * strategy: 'priority',
354
+ * cooldownMs: 30_000,
355
+ * });
356
+ * ```
357
+ *
358
+ * @example RequestRouter
359
+ * ```typescript
360
+ * import { createDefaultRequestRouter } from '@juspay/neurolink';
361
+ *
362
+ * const router = createDefaultRequestRouter({
363
+ * visionTier: { provider: 'vertex', model: 'gemini-2.5-pro' },
364
+ * largeTier: { provider: 'anthropic', model: 'claude-opus-4-5' },
365
+ * smallTier: { provider: 'anthropic', model: 'claude-haiku-3-5' },
366
+ * });
367
+ * ```
368
+ */
369
+ export { classifyProviderError, ModelPool, createDefaultRequestRouter, } from "./routing/index.js";
339
370
  /**
340
371
  * Server Adapters for exposing NeuroLink as HTTP APIs
341
372
  *
package/dist/lib/index.js CHANGED
@@ -354,6 +354,8 @@ neuroLinkToolToMCP, mcpToolToNeuroLink, batchConvertToMCP, batchConvertToNeuroLi
354
354
  ToolIntegrationManager, globalToolIntegrationManager, wrapToolWithElicitation, wrapToolsWithElicitation, createElicitationContext, confirmationMiddleware, validationMiddleware, loggingMiddleware, createRetryMiddleware, createTimeoutMiddleware, createToolMiddlewareChain,
355
355
  // MCP Enhancements - Elicitation Protocol
356
356
  ElicitationProtocolAdapter, globalElicitationProtocol, isElicitationProtocolMessage, protocolMessageToElicitation, elicitationResponseToProtocol, createElicitationRequest, createElicitationResponse, createElicitationCancel, createTextInputRequest, createSelectRequest, createConfirmationRequest, createFormRequest, } from "./mcp/index.js";
357
+ // Tool signature deduplication utilities (opt-in, fail-open)
358
+ export { computeToolSignature, dedupeTools } from "./core/toolDedup.js";
357
359
  export { logger } from "./utils/logger.js";
358
360
  export { getPoolStats } from "./utils/redis.js";
359
361
  // Pre-call tool routing cache (ITEM C: LRU+TTL cache + session stickiness)
@@ -539,6 +541,39 @@ export { AGGRESSIVE_FALLBACK_WORKFLOW, FAST_FALLBACK_WORKFLOW, } from "./workflo
539
541
  // Pre-built workflows - Multi-judge
540
542
  export { createMultiJudgeWorkflow, MULTI_JUDGE_3_WORKFLOW, MULTI_JUDGE_5_WORKFLOW, } from "./workflow/workflows/multiJudgeWorkflow.js";
541
543
  // ============================================================================
544
+ // MODEL POOL & REQUEST ROUTER - Pluggable pre-call routing + failover
545
+ // ============================================================================
546
+ /**
547
+ * ModelPool and RequestRouter — opt-in multi-provider failover with
548
+ * error-class-aware cooldown, and a pluggable pre-call provider/model router.
549
+ *
550
+ * @example ModelPool
551
+ * ```typescript
552
+ * import { ModelPool, classifyProviderError } from '@juspay/neurolink';
553
+ *
554
+ * const pool = new ModelPool({
555
+ * members: [
556
+ * { provider: 'anthropic', model: 'claude-sonnet-4-5' },
557
+ * { provider: 'vertex', model: 'gemini-2.5-flash' },
558
+ * ],
559
+ * strategy: 'priority',
560
+ * cooldownMs: 30_000,
561
+ * });
562
+ * ```
563
+ *
564
+ * @example RequestRouter
565
+ * ```typescript
566
+ * import { createDefaultRequestRouter } from '@juspay/neurolink';
567
+ *
568
+ * const router = createDefaultRequestRouter({
569
+ * visionTier: { provider: 'vertex', model: 'gemini-2.5-pro' },
570
+ * largeTier: { provider: 'anthropic', model: 'claude-opus-4-5' },
571
+ * smallTier: { provider: 'anthropic', model: 'claude-haiku-3-5' },
572
+ * });
573
+ * ```
574
+ */
575
+ export { classifyProviderError, ModelPool, createDefaultRequestRouter, } from "./routing/index.js";
576
+ // ============================================================================
542
577
  // SERVER ADAPTERS - HTTP API Framework Integration
543
578
  // ============================================================================
544
579
  // Server Types
@@ -5,7 +5,7 @@
5
5
  * Enhanced AI provider system with natural MCP tool access.
6
6
  * Uses real MCP infrastructure for tool discovery and execution.
7
7
  */
8
- import type { CompactionConfig, CompactionResult, SpanData, ObservabilityConfig, MetricsSummary, MCPToolAnnotations, TraceView, AuthenticatedContext, AuthProvider, JsonObject, NeuroLinkEvents, TypedEventEmitter, MCPEnhancementsConfig, NeuroLinkAuthConfig, NeurolinkConstructorConfig, ChatMessage, ExternalMCPOperationResult, ExternalMCPServerInstance, ExternalMCPToolInfo, GenerateOptions, GenerateResult, ProviderStatus, TextGenerationOptions, TextGenerationResult, MCPExecutableTool, MCPServerInfo, MCPStatus, StreamOptions, StreamResult, ToolExecutionContext, ToolExecutionSummary, ToolInfo, ToolRegistrationOptions, BatchOperationResult, StreamGenerationEndContext, ToolRoutingServerDescriptor } from "./types/index.js";
8
+ import type { CompactionConfig, CompactionResult, SpanData, ObservabilityConfig, MetricsSummary, MCPToolAnnotations, TraceView, AuthenticatedContext, AuthProvider, JsonObject, NeuroLinkEvents, TypedEventEmitter, MCPEnhancementsConfig, NeuroLinkAuthConfig, NeurolinkConstructorConfig, ChatMessage, ExternalMCPOperationResult, ExternalMCPServerInstance, ExternalMCPToolInfo, GenerateOptions, GenerateResult, ProviderStatus, TextGenerationOptions, TextGenerationResult, MCPExecutableTool, MCPServerInfo, MCPStatus, StreamOptions, StreamResult, ToolExecutionContext, ToolExecutionSummary, ToolInfo, ToolRegistrationOptions, BatchOperationResult, StreamGenerationEndContext, ToolRoutingServerDescriptor, ToolDedupConfig } from "./types/index.js";
9
9
  import { ConversationMemoryManager } from "./core/conversationMemoryManager.js";
10
10
  import type { RedisConversationMemoryManager } from "./core/redisConversationMemoryManager.js";
11
11
  import { ExternalServerManager } from "./mcp/externalServerManager.js";
@@ -102,12 +102,15 @@ export declare class NeuroLink {
102
102
  private conversationMemoryConfig?;
103
103
  private toolRoutingConfig?;
104
104
  private toolRoutingCacheInstance?;
105
+ private toolDedupConfig?;
105
106
  private enableOrchestration;
106
107
  private authProvider?;
107
108
  private pendingAuthConfig?;
108
109
  private authInitPromise?;
109
110
  private credentials?;
110
111
  private readonly fallbackConfig;
112
+ private readonly modelPool;
113
+ private readonly requestRouter;
111
114
  /**
112
115
  * Merge instance-level credentials with per-call credentials.
113
116
  *
@@ -611,6 +614,25 @@ export declare class NeuroLink {
611
614
  private maybeHandleEarlyGenerateResult;
612
615
  private runStandardGenerateRequest;
613
616
  private maybeApplyGenerateOrchestration;
617
+ /**
618
+ * Applies the host-configured `requestRouter` to `options` in place.
619
+ *
620
+ * The router is skipped when:
621
+ * - no `requestRouter` is configured on this instance, or
622
+ * - the caller explicitly set both `options.provider` AND `options.model`
623
+ * (we only skip the router if BOTH are set; a caller setting only one
624
+ * still lets the router fill in the other field).
625
+ *
626
+ * Fails open: any router error is logged at WARN level and the call
627
+ * continues with the original options unmodified.
628
+ *
629
+ * @param options — the mutable options object (generate or stream).
630
+ * @param promptText — the text prompt used to build the RouterInputContext.
631
+ * @param hasTools — true when at least one tool is available for this call.
632
+ * @param requiresVision — true when the call includes image attachments.
633
+ * @param thinkingLevel — optional thinking level string from the call options.
634
+ */
635
+ private applyRequestRouter;
614
636
  private prepareGenerateAugmentations;
615
637
  private buildGenerateTextOptions;
616
638
  private finalizeGenerateRequestResult;
@@ -1089,6 +1111,19 @@ export declare class NeuroLink {
1089
1111
  * @see {@link NeuroLink.executeTool} for events related to tool execution
1090
1112
  */
1091
1113
  getEventEmitter(): TypedEventEmitter<NeuroLinkEvents>;
1114
+ /**
1115
+ * Returns the instance-level tool-dedup configuration, or `undefined` when
1116
+ * toolDedup was not provided at construction time.
1117
+ *
1118
+ * The stored object is returned as-is whenever `toolDedup` was supplied,
1119
+ * including when `enabled: false` — only the complete absence of a `toolDedup`
1120
+ * option results in `undefined`.
1121
+ *
1122
+ * Called by `BaseProvider.applyToolFiltering` so the dedup pass uses the
1123
+ * same config for every generate/stream call without threading an extra
1124
+ * parameter through the full call stack.
1125
+ */
1126
+ getToolDedupConfig(): ToolDedupConfig | undefined;
1092
1127
  /**
1093
1128
  * Curator P1-1: synchronous credential health check for a single provider.
1094
1129
  *