@juspay/neurolink 11.2.3 → 11.3.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 +396 -396
- package/dist/core/handlerRegistry.d.ts +29 -0
- package/dist/core/handlerRegistry.js +61 -0
- package/dist/core/loopEngine.d.ts +23 -0
- package/dist/core/loopEngine.js +245 -0
- package/dist/core/nativeToolFormat.d.ts +33 -0
- package/dist/core/nativeToolFormat.js +30 -0
- package/dist/core/streamChannel.d.ts +10 -0
- package/dist/core/streamChannel.js +76 -0
- package/dist/factories/providerRegistry.d.ts +4 -16
- package/dist/factories/providerRegistry.js +4 -56
- package/dist/lib/core/handlerRegistry.d.ts +29 -0
- package/dist/lib/core/handlerRegistry.js +62 -0
- package/dist/lib/core/loopEngine.d.ts +23 -0
- package/dist/lib/core/loopEngine.js +246 -0
- package/dist/lib/core/nativeToolFormat.d.ts +33 -0
- package/dist/lib/core/nativeToolFormat.js +31 -0
- package/dist/lib/core/streamChannel.d.ts +10 -0
- package/dist/lib/core/streamChannel.js +77 -0
- package/dist/lib/factories/providerRegistry.d.ts +4 -16
- package/dist/lib/factories/providerRegistry.js +4 -56
- package/dist/lib/providers/anthropic/cacheControl.d.ts +12 -0
- package/dist/lib/providers/anthropic/cacheControl.js +15 -0
- package/dist/lib/providers/anthropic/client.js +12 -45
- package/dist/lib/providers/googleAiStudio/client.js +7 -5
- package/dist/lib/providers/googleNativeGemini3/utils.d.ts +4 -11
- package/dist/lib/providers/googleNativeGemini3/utils.js +1 -75
- package/dist/lib/providers/googleVertex/client.js +28 -30
- package/dist/lib/providers/openaiChatCompletionsBase.js +10 -12
- package/dist/lib/providers/openaiChatCompletionsClient.d.ts +1 -5
- package/dist/lib/providers/openaiChatCompletionsClient.js +0 -26
- package/dist/lib/types/index.d.ts +3 -0
- package/dist/lib/types/index.js +3 -0
- package/dist/lib/types/loopEngine.d.ts +78 -0
- package/dist/lib/types/loopEngine.js +2 -0
- package/dist/lib/types/nativeTools.d.ts +16 -0
- package/dist/lib/types/nativeTools.js +2 -0
- package/dist/lib/types/openaiCompatible.d.ts +2 -2
- package/dist/lib/types/providers.d.ts +0 -13
- package/dist/lib/types/streaming.d.ts +15 -0
- package/dist/lib/types/streaming.js +2 -0
- package/dist/lib/utils/avatarProcessor.d.ts +5 -1
- package/dist/lib/utils/avatarProcessor.js +13 -18
- package/dist/lib/utils/musicProcessor.d.ts +5 -1
- package/dist/lib/utils/musicProcessor.js +13 -18
- package/dist/lib/utils/sttProcessor.d.ts +10 -2
- package/dist/lib/utils/sttProcessor.js +22 -18
- package/dist/lib/utils/ttsProcessor.d.ts +10 -2
- package/dist/lib/utils/ttsProcessor.js +22 -18
- package/dist/lib/utils/videoProcessor.d.ts +5 -1
- package/dist/lib/utils/videoProcessor.js +13 -18
- package/dist/lib/voice/RealtimeVoiceAPI.d.ts +1 -1
- package/dist/lib/voice/RealtimeVoiceAPI.js +17 -28
- package/dist/providers/anthropic/cacheControl.d.ts +12 -0
- package/dist/providers/anthropic/cacheControl.js +14 -0
- package/dist/providers/anthropic/client.js +12 -45
- package/dist/providers/googleAiStudio/client.js +7 -5
- package/dist/providers/googleNativeGemini3/utils.d.ts +4 -11
- package/dist/providers/googleNativeGemini3/utils.js +1 -75
- package/dist/providers/googleVertex/client.js +28 -30
- package/dist/providers/openaiChatCompletionsBase.js +10 -12
- package/dist/providers/openaiChatCompletionsClient.d.ts +1 -5
- package/dist/providers/openaiChatCompletionsClient.js +0 -26
- package/dist/types/index.d.ts +3 -0
- package/dist/types/index.js +3 -0
- package/dist/types/loopEngine.d.ts +78 -0
- package/dist/types/loopEngine.js +1 -0
- package/dist/types/nativeTools.d.ts +16 -0
- package/dist/types/nativeTools.js +1 -0
- package/dist/types/openaiCompatible.d.ts +2 -2
- package/dist/types/providers.d.ts +0 -13
- package/dist/types/streaming.d.ts +15 -0
- package/dist/types/streaming.js +1 -0
- package/dist/utils/avatarProcessor.d.ts +5 -1
- package/dist/utils/avatarProcessor.js +13 -18
- package/dist/utils/musicProcessor.d.ts +5 -1
- package/dist/utils/musicProcessor.js +13 -18
- package/dist/utils/sttProcessor.d.ts +10 -2
- package/dist/utils/sttProcessor.js +22 -18
- package/dist/utils/ttsProcessor.d.ts +10 -2
- package/dist/utils/ttsProcessor.js +22 -18
- package/dist/utils/videoProcessor.d.ts +5 -1
- package/dist/utils/videoProcessor.js +13 -18
- package/dist/voice/RealtimeVoiceAPI.d.ts +1 -1
- package/dist/voice/RealtimeVoiceAPI.js +17 -28
- package/package.json +9 -1
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { logger } from "../utils/logger.js";
|
|
2
|
+
import { sanitizeForLog } from "../utils/logSanitize.js";
|
|
3
|
+
/**
|
|
4
|
+
* Generic provider-name → handler registry shared by every media-generation
|
|
5
|
+
* ecosystem (TTS, STT, Realtime, Video, Music, Avatar). Each ecosystem's own
|
|
6
|
+
* processor class composes one instance of this class instead of hand-rolling
|
|
7
|
+
* its own `Map<string, THandler>` plus register/supports/get/list methods.
|
|
8
|
+
*
|
|
9
|
+
* Centralizes only the behavior that was byte-identical across all six
|
|
10
|
+
* hand-rolled registries: input validation, name normalization (lowercase),
|
|
11
|
+
* the overwrite-warning log line, and the four lookup/list/clear operations.
|
|
12
|
+
* Registration-outcome debug logging (whose exact phrasing differs per
|
|
13
|
+
* ecosystem — e.g. "Registered TTS handler..." vs "Registered video
|
|
14
|
+
* handler...") and any ecosystem-specific extra logging (e.g. TTS/STT's
|
|
15
|
+
* `supports()` diagnostics) stay in the owning processor's own wrapper
|
|
16
|
+
* methods; this class does not attempt to unify those.
|
|
17
|
+
*/
|
|
18
|
+
export class HandlerRegistry {
|
|
19
|
+
scopeName;
|
|
20
|
+
handlers = new Map();
|
|
21
|
+
/**
|
|
22
|
+
* @param scopeName Log-line prefix, e.g. "TTSProcessor" — matches the
|
|
23
|
+
* `[ClassName]` prefix each processor already uses in its own logs.
|
|
24
|
+
*/
|
|
25
|
+
constructor(scopeName) {
|
|
26
|
+
this.scopeName = scopeName;
|
|
27
|
+
}
|
|
28
|
+
register(providerName, handler) {
|
|
29
|
+
if (!providerName) {
|
|
30
|
+
throw new Error("Provider name is required");
|
|
31
|
+
}
|
|
32
|
+
if (!handler) {
|
|
33
|
+
throw new Error("Handler is required");
|
|
34
|
+
}
|
|
35
|
+
const key = providerName.toLowerCase();
|
|
36
|
+
if (this.handlers.has(key)) {
|
|
37
|
+
// Every caller today passes a literal provider slug, but this class is
|
|
38
|
+
// generic over an arbitrary string — sanitize before logging it so a
|
|
39
|
+
// future caller building the key from untrusted input can't leak a
|
|
40
|
+
// bearer token/API key through this warning.
|
|
41
|
+
logger.warn(`[${this.scopeName}] Overwriting existing handler for provider: ${sanitizeForLog(key)}`);
|
|
42
|
+
}
|
|
43
|
+
this.handlers.set(key, handler);
|
|
44
|
+
}
|
|
45
|
+
supports(providerName) {
|
|
46
|
+
if (!providerName) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
return this.handlers.has(providerName.toLowerCase());
|
|
50
|
+
}
|
|
51
|
+
get(providerName) {
|
|
52
|
+
return this.handlers.get(providerName.toLowerCase());
|
|
53
|
+
}
|
|
54
|
+
list() {
|
|
55
|
+
return Array.from(this.handlers.keys());
|
|
56
|
+
}
|
|
57
|
+
clear() {
|
|
58
|
+
this.handlers.clear();
|
|
59
|
+
logger.debug(`[${this.scopeName}] Cleared all handlers`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=handlerRegistry.js.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { AgenticLoopAdapter, AgenticLoopOptions, AgenticLoopResult } from "../types/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Run one adapter-parameterized agentic tool-calling turn. Owns the
|
|
4
|
+
* maxSteps-bounded loop, generic tool dispatch (with an opt-in
|
|
5
|
+
* TOOL_NOT_FOUND/failure-strike breaker — see AgenticLoopAdapter.toolFailureBreaker),
|
|
6
|
+
* per-step usage accumulation, a single optional malformed-call retry,
|
|
7
|
+
* chunk emission through streamChannel, and a pre-first-chunk 429/5xx
|
|
8
|
+
* retry (via withProviderRetry) around every adapter.executeStep() call.
|
|
9
|
+
* The retry wrap is unconditional and adapter-agnostic — every migrated
|
|
10
|
+
* provider gets it for free, not just the ones that had a hand-rolled
|
|
11
|
+
* version before migration (see Verified Fact 4-adjacent note in Task 4
|
|
12
|
+
* Step 1 and the Risks & Rollback "Deliberate behavior changes" list for
|
|
13
|
+
* which families are gaining this for the first time). Everything
|
|
14
|
+
* wire-format-specific (building the request, parsing the SDK response,
|
|
15
|
+
* serializing tool results back into the conversation, mapping the raw
|
|
16
|
+
* stop reason) is delegated to `adapter`.
|
|
17
|
+
*/
|
|
18
|
+
export declare function runAgenticLoop<TConversation>(adapter: AgenticLoopAdapter<TConversation>, initialConversation: TConversation, options: AgenticLoopOptions): {
|
|
19
|
+
stream: AsyncIterable<{
|
|
20
|
+
content: string;
|
|
21
|
+
}>;
|
|
22
|
+
resultPromise: Promise<AgenticLoopResult<TConversation>>;
|
|
23
|
+
};
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { createStreamChannel } from "./streamChannel.js";
|
|
2
|
+
import { logger } from "../utils/logger.js";
|
|
3
|
+
import { withProviderRetry } from "../utils/providerRetry.js";
|
|
4
|
+
/**
|
|
5
|
+
* Marks a step error that occurred AFTER at least one chunk had already
|
|
6
|
+
* been streamed to the consumer for this step. Retrying at that point
|
|
7
|
+
* would duplicate or interleave already-emitted output, so this wrapper
|
|
8
|
+
* deliberately carries none of the original error's status/retry
|
|
9
|
+
* metadata (`.statusCode`/`.status`, no APICallError/NeuroLinkError
|
|
10
|
+
* branding) — that makes `withProviderRetry`'s internal
|
|
11
|
+
* `isRetryableProviderError()` check return false via its duck-typed
|
|
12
|
+
* fallback, which ends the retry loop on the very next classification
|
|
13
|
+
* instead of sleeping and re-invoking `adapter.executeStep`. The engine
|
|
14
|
+
* unwraps back to the original `cause` before it ever reaches the
|
|
15
|
+
* caller — see the try/catch around the `withProviderRetry` call below.
|
|
16
|
+
*/
|
|
17
|
+
class PostEmissionStepError extends Error {
|
|
18
|
+
cause;
|
|
19
|
+
constructor(cause) {
|
|
20
|
+
super(cause instanceof Error ? cause.message : String(cause));
|
|
21
|
+
this.cause = cause;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function sumUsage(a, b) {
|
|
25
|
+
return {
|
|
26
|
+
inputTokens: a.inputTokens + b.inputTokens,
|
|
27
|
+
outputTokens: a.outputTokens + b.outputTokens,
|
|
28
|
+
cacheReadTokens: (a.cacheReadTokens ?? 0) + (b.cacheReadTokens ?? 0) || undefined,
|
|
29
|
+
cacheWriteTokens: (a.cacheWriteTokens ?? 0) + (b.cacheWriteTokens ?? 0) || undefined,
|
|
30
|
+
reasoningTokens: (a.reasoningTokens ?? 0) + (b.reasoningTokens ?? 0) || undefined,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Run one adapter-parameterized agentic tool-calling turn. Owns the
|
|
35
|
+
* maxSteps-bounded loop, generic tool dispatch (with an opt-in
|
|
36
|
+
* TOOL_NOT_FOUND/failure-strike breaker — see AgenticLoopAdapter.toolFailureBreaker),
|
|
37
|
+
* per-step usage accumulation, a single optional malformed-call retry,
|
|
38
|
+
* chunk emission through streamChannel, and a pre-first-chunk 429/5xx
|
|
39
|
+
* retry (via withProviderRetry) around every adapter.executeStep() call.
|
|
40
|
+
* The retry wrap is unconditional and adapter-agnostic — every migrated
|
|
41
|
+
* provider gets it for free, not just the ones that had a hand-rolled
|
|
42
|
+
* version before migration (see Verified Fact 4-adjacent note in Task 4
|
|
43
|
+
* Step 1 and the Risks & Rollback "Deliberate behavior changes" list for
|
|
44
|
+
* which families are gaining this for the first time). Everything
|
|
45
|
+
* wire-format-specific (building the request, parsing the SDK response,
|
|
46
|
+
* serializing tool results back into the conversation, mapping the raw
|
|
47
|
+
* stop reason) is delegated to `adapter`.
|
|
48
|
+
*/
|
|
49
|
+
export function runAgenticLoop(adapter, initialConversation, options) {
|
|
50
|
+
const channel = createStreamChannel();
|
|
51
|
+
const internalAbort = new AbortController();
|
|
52
|
+
const onCallerAbort = () => internalAbort.abort();
|
|
53
|
+
options.abortSignal?.addEventListener("abort", onCallerAbort);
|
|
54
|
+
if (options.abortSignal?.aborted) {
|
|
55
|
+
internalAbort.abort();
|
|
56
|
+
}
|
|
57
|
+
const failedTools = new Map();
|
|
58
|
+
let malformedRetryUsed = false;
|
|
59
|
+
const resultPromise = (async () => {
|
|
60
|
+
let conversation = initialConversation;
|
|
61
|
+
let usage = { inputTokens: 0, outputTokens: 0 };
|
|
62
|
+
let finalText = "";
|
|
63
|
+
let rawStopReason;
|
|
64
|
+
const allToolCalls = [];
|
|
65
|
+
const allToolExecutions = [];
|
|
66
|
+
let hadToolCallsAtCap = false;
|
|
67
|
+
try {
|
|
68
|
+
for (let step = 0; step < adapter.maxSteps; step++) {
|
|
69
|
+
if (internalAbort.signal.aborted) {
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
if (adapter.planReclaim) {
|
|
73
|
+
const reclaimed = adapter.planReclaim(conversation, step);
|
|
74
|
+
if (reclaimed) {
|
|
75
|
+
conversation = reclaimed.conversation;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const request = adapter.buildStepRequest(conversation, step);
|
|
79
|
+
// Pre-first-chunk 429/5xx retry: watch whether THIS attempt of
|
|
80
|
+
// THIS step pushes anything to the shared channel before it
|
|
81
|
+
// throws. `hasEmitted` resets at the top of every attempt
|
|
82
|
+
// withProviderRetry makes; the instant an attempt emits and then
|
|
83
|
+
// throws, the thrown error is rewrapped as a PostEmissionStepError
|
|
84
|
+
// (no status/branding info survives the rewrap), which
|
|
85
|
+
// isRetryableProviderError() duck-types as non-retryable — so
|
|
86
|
+
// withProviderRetry gives up immediately instead of sleeping and
|
|
87
|
+
// re-invoking executeStep, which would duplicate/interleave
|
|
88
|
+
// output already sent to the consumer. The original error (not
|
|
89
|
+
// the wrapper) is what the caller of runAgenticLoop ultimately
|
|
90
|
+
// sees, via the unwrap in the catch below.
|
|
91
|
+
let hasEmitted = false;
|
|
92
|
+
const watchedChannel = {
|
|
93
|
+
push: (chunk) => {
|
|
94
|
+
hasEmitted = true;
|
|
95
|
+
channel.push(chunk);
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
let stepResult;
|
|
99
|
+
try {
|
|
100
|
+
stepResult = await withProviderRetry(async () => {
|
|
101
|
+
hasEmitted = false;
|
|
102
|
+
try {
|
|
103
|
+
return await adapter.executeStep(request, watchedChannel, internalAbort.signal);
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
throw hasEmitted ? new PostEmissionStepError(err) : err;
|
|
107
|
+
}
|
|
108
|
+
}, undefined, // no OTel span threaded through the engine today; adapters instrument their own steps if they need span-level detail
|
|
109
|
+
`${adapter.providerLabel}.step`);
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
throw err instanceof PostEmissionStepError ? err.cause : err;
|
|
113
|
+
}
|
|
114
|
+
usage = sumUsage(usage, stepResult.usage);
|
|
115
|
+
rawStopReason = stepResult.rawStopReason;
|
|
116
|
+
if (adapter.isMalformedStep?.(stepResult) &&
|
|
117
|
+
!malformedRetryUsed &&
|
|
118
|
+
!internalAbort.signal.aborted) {
|
|
119
|
+
malformedRetryUsed = true;
|
|
120
|
+
logger.warn(`[${adapter.providerLabel}] Malformed function call at step ${step + 1}/${adapter.maxSteps}; retrying once.`);
|
|
121
|
+
conversation =
|
|
122
|
+
adapter.buildMalformedRetryNote?.(conversation) ?? conversation;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (stepResult.toolCalls.length === 0) {
|
|
126
|
+
finalText = stepResult.text || finalText;
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
if (step === adapter.maxSteps - 1) {
|
|
130
|
+
hadToolCallsAtCap = true;
|
|
131
|
+
}
|
|
132
|
+
const toolResults = [];
|
|
133
|
+
for (const call of stepResult.toolCalls) {
|
|
134
|
+
allToolCalls.push(call);
|
|
135
|
+
const breaker = adapter.toolFailureBreaker;
|
|
136
|
+
const failInfo = breaker ? failedTools.get(call.name) : undefined;
|
|
137
|
+
if (breaker && failInfo && failInfo.count >= breaker.maxRetries) {
|
|
138
|
+
const output = {
|
|
139
|
+
error: `TOOL_PERMANENTLY_FAILED: "${call.name}" has failed ${failInfo.count} times. Last error: ${failInfo.lastError}.`,
|
|
140
|
+
status: "permanently_failed",
|
|
141
|
+
do_not_retry: true,
|
|
142
|
+
};
|
|
143
|
+
toolResults.push({
|
|
144
|
+
...call,
|
|
145
|
+
output,
|
|
146
|
+
error: output.error,
|
|
147
|
+
permanentlyFailed: true,
|
|
148
|
+
});
|
|
149
|
+
allToolExecutions.push({
|
|
150
|
+
name: call.name,
|
|
151
|
+
input: call.args,
|
|
152
|
+
output,
|
|
153
|
+
});
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
const tool = options.tools?.[call.name];
|
|
157
|
+
if (!tool?.execute) {
|
|
158
|
+
const output = breaker
|
|
159
|
+
? {
|
|
160
|
+
error: `TOOL_NOT_FOUND: "${call.name}" does not exist.`,
|
|
161
|
+
status: "permanently_failed",
|
|
162
|
+
do_not_retry: true,
|
|
163
|
+
}
|
|
164
|
+
: { error: `Tool not found: ${call.name}` };
|
|
165
|
+
toolResults.push({
|
|
166
|
+
...call,
|
|
167
|
+
output,
|
|
168
|
+
error: output.error,
|
|
169
|
+
permanentlyFailed: !!breaker,
|
|
170
|
+
});
|
|
171
|
+
allToolExecutions.push({
|
|
172
|
+
name: call.name,
|
|
173
|
+
input: call.args,
|
|
174
|
+
output,
|
|
175
|
+
});
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
try {
|
|
179
|
+
const output = await tool.execute(call.args, {
|
|
180
|
+
toolCallId: call.id,
|
|
181
|
+
abortSignal: internalAbort.signal,
|
|
182
|
+
});
|
|
183
|
+
toolResults.push({ ...call, output });
|
|
184
|
+
allToolExecutions.push({
|
|
185
|
+
name: call.name,
|
|
186
|
+
input: call.args,
|
|
187
|
+
output,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
catch (err) {
|
|
191
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
192
|
+
if (breaker) {
|
|
193
|
+
const current = failedTools.get(call.name) ?? {
|
|
194
|
+
count: 0,
|
|
195
|
+
lastError: "",
|
|
196
|
+
};
|
|
197
|
+
current.count++;
|
|
198
|
+
current.lastError = message;
|
|
199
|
+
failedTools.set(call.name, current);
|
|
200
|
+
}
|
|
201
|
+
const output = { error: message, status: "failed" };
|
|
202
|
+
toolResults.push({ ...call, output, error: message });
|
|
203
|
+
allToolExecutions.push({
|
|
204
|
+
name: call.name,
|
|
205
|
+
input: call.args,
|
|
206
|
+
output,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
conversation = adapter.buildToolResultMessages(conversation, stepResult, toolResults);
|
|
211
|
+
}
|
|
212
|
+
const finishReason = adapter.mapFinishReason(rawStopReason, hadToolCallsAtCap);
|
|
213
|
+
return {
|
|
214
|
+
text: finalText,
|
|
215
|
+
toolCalls: allToolCalls,
|
|
216
|
+
toolExecutions: allToolExecutions,
|
|
217
|
+
usage,
|
|
218
|
+
finishReason,
|
|
219
|
+
rawStopReason,
|
|
220
|
+
conversation,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
catch (err) {
|
|
224
|
+
// Must run before the finally block's channel.close(): a consumer
|
|
225
|
+
// parked in the channel's iterable is woken by whichever of
|
|
226
|
+
// error()/close() runs first, and close() alone would let a
|
|
227
|
+
// stream-only consumer observe a clean end of stream for a turn that
|
|
228
|
+
// actually failed. Calling error() here — synchronously, inside this
|
|
229
|
+
// catch — guarantees it lands before close(), with no dependence on
|
|
230
|
+
// microtask scheduling (unlike an outer promise-chained catch handler
|
|
231
|
+
// on this IIFE, which would run in a later microtask after `finally`
|
|
232
|
+
// already closed the channel).
|
|
233
|
+
channel.error(err);
|
|
234
|
+
throw err;
|
|
235
|
+
}
|
|
236
|
+
finally {
|
|
237
|
+
// close() after error() is harmless: it only flips `done`, and
|
|
238
|
+
// streamChannel.error() keeps its error state intact regardless of a
|
|
239
|
+
// later close() call.
|
|
240
|
+
channel.close();
|
|
241
|
+
options.abortSignal?.removeEventListener("abort", onCallerAbort);
|
|
242
|
+
}
|
|
243
|
+
})();
|
|
244
|
+
return { stream: channel.iterable, resultPromise };
|
|
245
|
+
}
|
|
246
|
+
//# sourceMappingURL=loopEngine.js.map
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { NativeAnthropicToolDeclaration, NativeToolDeclarationsResult, Tool } from "../types/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Convert a NeuroLink tool record into the wire-format a native
|
|
4
|
+
* (non-AI-SDK) provider SDK expects. Absorbs the direct-Anthropic
|
|
5
|
+
* provider's `toolsToAnthropic` — previously duplicated between the
|
|
6
|
+
* streaming loop's pre-loop snapshot and its mid-turn discovery-hydration
|
|
7
|
+
* call site (both in anthropic/client.ts). Gemini's `functionDeclarations`
|
|
8
|
+
* shape was already centralized in `buildNativeToolDeclarations`; this
|
|
9
|
+
* function is a thin facade over it so every native provider calls one
|
|
10
|
+
* entry point.
|
|
11
|
+
*
|
|
12
|
+
* Scope note: Vertex's Claude-on-Vertex `input_schema` builder
|
|
13
|
+
* (`buildAnthropicToolDeclaration` in googleVertex/client.ts) is
|
|
14
|
+
* deliberately NOT routed through this function. Despite the superficial
|
|
15
|
+
* similarity, it is not a byte-for-byte duplicate of `toolsToAnthropic`: it
|
|
16
|
+
* strips the converted schema down to `{type, properties, required}` only
|
|
17
|
+
* (dropping any other JSON-Schema keywords `convertZodToJsonSchema` may
|
|
18
|
+
* produce), it always runs `inlineJsonSchema` (Anthropic-direct never has),
|
|
19
|
+
* it prefers `parameters` over `inputSchema` (the opposite fallback order
|
|
20
|
+
* from Anthropic-direct), and it has no `cache_control` support of its own
|
|
21
|
+
* because Vertex applies cache breakpoints later via the separate
|
|
22
|
+
* `applyVertexAnthropicCacheBreakpoints` pass. Collapsing these into one
|
|
23
|
+
* shared implementation would risk a live tool-calling regression on one
|
|
24
|
+
* provider or the other; see the Task 2 report for the full comparison.
|
|
25
|
+
*
|
|
26
|
+
* These are genuine TS overload declarations, not redeclarations: the base
|
|
27
|
+
* `no-redeclare` ESLint rule doesn't understand the overload-signatures +
|
|
28
|
+
* implementation pattern (the TS-aware `@typescript-eslint/no-redeclare`
|
|
29
|
+
* variant would, but isn't enabled project-wide), so each signature below
|
|
30
|
+
* is individually exempted.
|
|
31
|
+
*/
|
|
32
|
+
export declare function toNativeToolDeclarations(tools: Record<string, Tool>, format: "input_schema"): NativeAnthropicToolDeclaration[] | undefined;
|
|
33
|
+
export declare function toNativeToolDeclarations(tools: Record<string, Tool>, format: "functionDeclarations"): NativeToolDeclarationsResult;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { buildNativeToolDeclarations } from "../providers/googleNativeGemini3/utils.js";
|
|
2
|
+
import { cacheControlOf } from "../providers/anthropic/cacheControl.js";
|
|
3
|
+
import { convertZodToJsonSchema } from "../utils/schemaConversion.js";
|
|
4
|
+
// eslint-disable-next-line no-redeclare -- TS overload implementation signature, not a redeclaration
|
|
5
|
+
export function toNativeToolDeclarations(tools, format) {
|
|
6
|
+
if (format === "functionDeclarations") {
|
|
7
|
+
return buildNativeToolDeclarations(tools);
|
|
8
|
+
}
|
|
9
|
+
const entries = Object.entries(tools ?? {});
|
|
10
|
+
if (entries.length === 0) {
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
return entries.map(([name, tool]) => {
|
|
14
|
+
const t = tool;
|
|
15
|
+
const rawSchema = t.inputSchema ?? t.parameters;
|
|
16
|
+
const input_schema = (rawSchema
|
|
17
|
+
? convertZodToJsonSchema(rawSchema)
|
|
18
|
+
: { type: "object", properties: {} });
|
|
19
|
+
// GenerationHandler marks the last tool definition with a cache
|
|
20
|
+
// breakpoint when prompt caching is active — keep honoring it.
|
|
21
|
+
const cc = cacheControlOf(tool);
|
|
22
|
+
const declaration = {
|
|
23
|
+
name,
|
|
24
|
+
...(t.description ? { description: t.description } : {}),
|
|
25
|
+
input_schema,
|
|
26
|
+
...(cc ? { cache_control: cc } : {}),
|
|
27
|
+
};
|
|
28
|
+
return declaration;
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=nativeToolFormat.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { StreamChannel } from "../types/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Create a push-based channel bridging a background producer (an agentic
|
|
4
|
+
* tool-calling loop) with an async-iterable consumer, enabling truly
|
|
5
|
+
* incremental streaming: values are yielded to the caller as they arrive
|
|
6
|
+
* rather than being buffered until the producer finishes.
|
|
7
|
+
*/
|
|
8
|
+
export declare function createStreamChannel<T = {
|
|
9
|
+
content: string;
|
|
10
|
+
}>(): StreamChannel<T>;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Create a push-based channel bridging a background producer (an agentic
|
|
3
|
+
* tool-calling loop) with an async-iterable consumer, enabling truly
|
|
4
|
+
* incremental streaming: values are yielded to the caller as they arrive
|
|
5
|
+
* rather than being buffered until the producer finishes.
|
|
6
|
+
*/
|
|
7
|
+
export function createStreamChannel() {
|
|
8
|
+
const queue = [];
|
|
9
|
+
let done = false;
|
|
10
|
+
let fatalError = undefined;
|
|
11
|
+
// Tracked separately from `fatalError`: a producer can legitimately
|
|
12
|
+
// reject with `undefined` (e.g. `throw undefined`), and `fatalError !==
|
|
13
|
+
// undefined` would then be indistinguishable from "no error occurred",
|
|
14
|
+
// closing the stream cleanly instead of surfacing the failure.
|
|
15
|
+
let hasError = false;
|
|
16
|
+
let notify = null;
|
|
17
|
+
function wake() {
|
|
18
|
+
if (notify) {
|
|
19
|
+
const fn = notify;
|
|
20
|
+
notify = null;
|
|
21
|
+
fn();
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function push(value) {
|
|
25
|
+
if (done) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
queue.push(value);
|
|
29
|
+
wake();
|
|
30
|
+
}
|
|
31
|
+
function close() {
|
|
32
|
+
done = true;
|
|
33
|
+
wake();
|
|
34
|
+
}
|
|
35
|
+
function error(err) {
|
|
36
|
+
done = true;
|
|
37
|
+
fatalError = err;
|
|
38
|
+
hasError = true;
|
|
39
|
+
wake();
|
|
40
|
+
}
|
|
41
|
+
let readIndex = 0;
|
|
42
|
+
async function* iterable() {
|
|
43
|
+
try {
|
|
44
|
+
while (true) {
|
|
45
|
+
if (readIndex < queue.length) {
|
|
46
|
+
yield queue[readIndex++];
|
|
47
|
+
// Periodically compact consumed entries to avoid unbounded retention.
|
|
48
|
+
if (readIndex > 1024 && readIndex * 2 >= queue.length) {
|
|
49
|
+
queue.splice(0, readIndex);
|
|
50
|
+
readIndex = 0;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
else if (done) {
|
|
54
|
+
if (hasError) {
|
|
55
|
+
throw fatalError instanceof Error
|
|
56
|
+
? fatalError
|
|
57
|
+
: new Error(String(fatalError));
|
|
58
|
+
}
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
await new Promise((resolve) => {
|
|
63
|
+
notify = resolve;
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
// Consumer stopped reading (disconnect/cancel): stop buffering.
|
|
70
|
+
done = true;
|
|
71
|
+
queue.length = 0;
|
|
72
|
+
notify?.();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return { push, close, error, iterable: iterable() };
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=streamChannel.js.map
|
|
@@ -1,14 +1,4 @@
|
|
|
1
1
|
import type { ProviderRegistryOptions } from "../types/index.js";
|
|
2
|
-
import { AIProviderName } from "../constants/enums.js";
|
|
3
|
-
/**
|
|
4
|
-
* Static module -> provider-ID manifest for every provider registered below.
|
|
5
|
-
* Module names (file/dir under src/lib/providers/) intentionally differ from
|
|
6
|
-
* their canonical IDs (e.g. amazonBedrock -> "bedrock", googleVertex ->
|
|
7
|
-
* "vertex"); static scanners that cannot resolve the dynamic import() calls
|
|
8
|
-
* use this mapping to confirm a module is registered. Enforced at runtime so
|
|
9
|
-
* the registry cannot silently drift from this manifest.
|
|
10
|
-
*/
|
|
11
|
-
export declare const PROVIDER_MODULE_TO_ID: Readonly<Record<string, AIProviderName>>;
|
|
12
2
|
/**
|
|
13
3
|
* Provider Registry - registers all providers with the factory
|
|
14
4
|
* This is where we migrate providers one by one to the new pattern
|
|
@@ -43,12 +33,10 @@ export declare class ProviderRegistry {
|
|
|
43
33
|
* (avoids circular dependencies; see CLAUDE.md).
|
|
44
34
|
*
|
|
45
35
|
* Not registered (by design): index.ts, providerTypeUtils.ts,
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
* (
|
|
49
|
-
*
|
|
50
|
-
* statically-scannable PROVIDER_MODULE_TO_ID manifest maps every registered
|
|
51
|
-
* module to its provider ID and is enforced below (Pattern Analysis #1178/#1317).
|
|
36
|
+
* anthropicBaseProvider.ts (legacy; anthropic.ts is live),
|
|
37
|
+
* googleNativeGemini3.ts (shared helpers). Filename != provider ID
|
|
38
|
+
* (e.g. amazonBedrock -> "bedrock"); static scanners that miss dynamic
|
|
39
|
+
* imports may false-positive (Pattern Analysis #1178).
|
|
52
40
|
*/
|
|
53
41
|
private static _doRegister;
|
|
54
42
|
/**
|
|
@@ -3,46 +3,6 @@ import { logger } from "../utils/logger.js";
|
|
|
3
3
|
import { AIProviderName, GoogleAIModels, OpenAIModels, AnthropicModels, VertexModels, OllamaModels, LiteLLMModels, HuggingFaceModels, DeepSeekModels, NvidiaNimModels, OpenRouterModels, CohereModels, VoyageModels, JinaModels, StabilityModels, IdeogramModels, RecraftModels, ReplicateModels, } from "../constants/enums.js";
|
|
4
4
|
import { PROVIDER_DESCRIPTORS_BY_NAME } from "./providerDescriptors.js";
|
|
5
5
|
import { OPENAI_COMPAT_CATALOG } from "../providers/openaiCompatCatalog.js";
|
|
6
|
-
/**
|
|
7
|
-
* Static module -> provider-ID manifest for every provider registered below.
|
|
8
|
-
* Module names (file/dir under src/lib/providers/) intentionally differ from
|
|
9
|
-
* their canonical IDs (e.g. amazonBedrock -> "bedrock", googleVertex ->
|
|
10
|
-
* "vertex"); static scanners that cannot resolve the dynamic import() calls
|
|
11
|
-
* use this mapping to confirm a module is registered. Enforced at runtime so
|
|
12
|
-
* the registry cannot silently drift from this manifest.
|
|
13
|
-
*/
|
|
14
|
-
export const PROVIDER_MODULE_TO_ID = {
|
|
15
|
-
amazonBedrock: AIProviderName.BEDROCK,
|
|
16
|
-
amazonSagemaker: AIProviderName.SAGEMAKER,
|
|
17
|
-
anthropic: AIProviderName.ANTHROPIC,
|
|
18
|
-
azureOpenai: AIProviderName.AZURE,
|
|
19
|
-
cloudflare: AIProviderName.CLOUDFLARE,
|
|
20
|
-
cohere: AIProviderName.COHERE,
|
|
21
|
-
deepseek: AIProviderName.DEEPSEEK,
|
|
22
|
-
fireworks: AIProviderName.FIREWORKS,
|
|
23
|
-
googleAiStudio: AIProviderName.GOOGLE_AI,
|
|
24
|
-
googleVertex: AIProviderName.VERTEX,
|
|
25
|
-
groq: AIProviderName.GROQ,
|
|
26
|
-
huggingFace: AIProviderName.HUGGINGFACE,
|
|
27
|
-
ideogram: AIProviderName.IDEOGRAM,
|
|
28
|
-
jina: AIProviderName.JINA,
|
|
29
|
-
litellm: AIProviderName.LITELLM,
|
|
30
|
-
llamaCpp: AIProviderName.LLAMACPP,
|
|
31
|
-
lmStudio: AIProviderName.LM_STUDIO,
|
|
32
|
-
mistral: AIProviderName.MISTRAL,
|
|
33
|
-
nvidiaNim: AIProviderName.NVIDIA_NIM,
|
|
34
|
-
ollama: AIProviderName.OLLAMA,
|
|
35
|
-
openAI: AIProviderName.OPENAI,
|
|
36
|
-
openaiCompatible: AIProviderName.OPENAI_COMPATIBLE,
|
|
37
|
-
openRouter: AIProviderName.OPENROUTER,
|
|
38
|
-
perplexity: AIProviderName.PERPLEXITY,
|
|
39
|
-
recraft: AIProviderName.RECRAFT,
|
|
40
|
-
replicate: AIProviderName.REPLICATE,
|
|
41
|
-
stability: AIProviderName.STABILITY,
|
|
42
|
-
togetherAi: AIProviderName.TOGETHER_AI,
|
|
43
|
-
voyage: AIProviderName.VOYAGE,
|
|
44
|
-
xai: AIProviderName.XAI,
|
|
45
|
-
};
|
|
46
6
|
/**
|
|
47
7
|
* Provider Registry - registers all providers with the factory
|
|
48
8
|
* This is where we migrate providers one by one to the new pattern
|
|
@@ -94,12 +54,10 @@ export class ProviderRegistry {
|
|
|
94
54
|
* (avoids circular dependencies; see CLAUDE.md).
|
|
95
55
|
*
|
|
96
56
|
* Not registered (by design): index.ts, providerTypeUtils.ts,
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
* (
|
|
100
|
-
*
|
|
101
|
-
* statically-scannable PROVIDER_MODULE_TO_ID manifest maps every registered
|
|
102
|
-
* module to its provider ID and is enforced below (Pattern Analysis #1178/#1317).
|
|
57
|
+
* anthropicBaseProvider.ts (legacy; anthropic.ts is live),
|
|
58
|
+
* googleNativeGemini3.ts (shared helpers). Filename != provider ID
|
|
59
|
+
* (e.g. amazonBedrock -> "bedrock"); static scanners that miss dynamic
|
|
60
|
+
* imports may false-positive (Pattern Analysis #1178).
|
|
103
61
|
*/
|
|
104
62
|
// eslint-disable-next-line max-lines-per-function
|
|
105
63
|
static async _doRegister() {
|
|
@@ -270,16 +228,6 @@ export class ProviderRegistry {
|
|
|
270
228
|
const { RecraftProvider } = await import("../providers/recraft.js");
|
|
271
229
|
return new RecraftProvider(modelName, sdk, undefined, recraftCreds);
|
|
272
230
|
}, process.env.RECRAFT_MODEL || RecraftModels.RECRAFT_V3, ["recraft"], PROVIDER_DESCRIPTORS_BY_NAME.get(AIProviderName.RECRAFT));
|
|
273
|
-
const unregistered = Object.entries(PROVIDER_MODULE_TO_ID).filter(([module, id]) => {
|
|
274
|
-
if (!ProviderFactory.hasProvider(id)) {
|
|
275
|
-
logger.error(`[ProviderRegistry] drift: module "${module}" not registered as "${id}"`);
|
|
276
|
-
return true;
|
|
277
|
-
}
|
|
278
|
-
return false;
|
|
279
|
-
});
|
|
280
|
-
if (unregistered.length > 0) {
|
|
281
|
-
throw new Error(`ProviderRegistry drift: ${unregistered.length} module(s) in PROVIDER_MODULE_TO_ID are not registered`);
|
|
282
|
-
}
|
|
283
231
|
logger.debug("All AI providers registered successfully");
|
|
284
232
|
// ===== TTS HANDLER REGISTRATION =====
|
|
285
233
|
try {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type Anthropic from "@anthropic-ai/sdk";
|
|
2
|
+
/**
|
|
3
|
+
* Read an Anthropic cache breakpoint from a message/part/tool carrier.
|
|
4
|
+
* MessageBuilder marks system messages (and GenerationHandler marks the last
|
|
5
|
+
* tool definition) with `providerOptions.anthropic.cacheControl` — the
|
|
6
|
+
* AI-SDK-era prompt-caching contract this native path must keep honoring.
|
|
7
|
+
*
|
|
8
|
+
* Extracted from anthropic/client.ts so `src/lib/core/nativeToolFormat.ts`
|
|
9
|
+
* can share it without importing the provider client (which would create a
|
|
10
|
+
* circular import: client.ts -> core/nativeToolFormat.ts -> client.ts).
|
|
11
|
+
*/
|
|
12
|
+
export declare const cacheControlOf: (carrier: unknown) => Anthropic.Messages.CacheControlEphemeral | undefined;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read an Anthropic cache breakpoint from a message/part/tool carrier.
|
|
3
|
+
* MessageBuilder marks system messages (and GenerationHandler marks the last
|
|
4
|
+
* tool definition) with `providerOptions.anthropic.cacheControl` — the
|
|
5
|
+
* AI-SDK-era prompt-caching contract this native path must keep honoring.
|
|
6
|
+
*
|
|
7
|
+
* Extracted from anthropic/client.ts so `src/lib/core/nativeToolFormat.ts`
|
|
8
|
+
* can share it without importing the provider client (which would create a
|
|
9
|
+
* circular import: client.ts -> core/nativeToolFormat.ts -> client.ts).
|
|
10
|
+
*/
|
|
11
|
+
export const cacheControlOf = (carrier) => {
|
|
12
|
+
const cc = carrier?.providerOptions?.anthropic?.cacheControl;
|
|
13
|
+
return cc?.type === "ephemeral" ? { type: "ephemeral" } : undefined;
|
|
14
|
+
};
|
|
15
|
+
//# sourceMappingURL=cacheControl.js.map
|