@juspay/neurolink 12.21.1 → 12.22.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 -4
- package/dist/browser/neurolink.min.js +398 -398
- package/dist/core/baseProvider.d.ts +18 -0
- package/dist/core/baseProvider.js +22 -0
- package/dist/core/modules/MessageBuilder.js +16 -0
- package/dist/factories/providerFactory.d.ts +20 -0
- package/dist/factories/providerFactory.js +100 -19
- package/dist/models/modelRegistry.d.ts +25 -0
- package/dist/models/modelRegistry.js +39 -0
- package/dist/providers/googleAiStudio/client.js +382 -50
- package/dist/providers/googleNativeGemini3/utils.d.ts +22 -0
- package/dist/providers/googleNativeGemini3/utils.js +24 -0
- package/dist/providers/googleVertex/client.d.ts +3 -12
- package/dist/providers/googleVertex/client.js +15 -34
- package/dist/providers/openaiChatCompletionsClient.js +8 -1
- package/dist/utils/stringDistance.d.ts +54 -0
- package/dist/utils/stringDistance.js +133 -0
- package/dist/utils/toolCallRepair.js +54 -55
- package/package.json +1 -1
|
@@ -460,6 +460,24 @@ export declare abstract class BaseProvider implements AIProvider {
|
|
|
460
460
|
* long-running generations that have always been allowed.
|
|
461
461
|
*/
|
|
462
462
|
protected withTurnTimeout<T>(options: TextGenerationOptions, descriptorGenerateMs: number | undefined, run: (timedOptions: TextGenerationOptions) => Promise<T>): Promise<T>;
|
|
463
|
+
/**
|
|
464
|
+
* Emit a `turn:lifecycle` event on the SDK emitter (alongside
|
|
465
|
+
* tool:start/tool:end) so loop conditions that previously only reached
|
|
466
|
+
* process logs — step-cap, time-limit, stall, abort, tool timeouts,
|
|
467
|
+
* malformed-call retries — are observable by consumers' own pipelines.
|
|
468
|
+
*
|
|
469
|
+
* Lives here rather than on one provider because every native agentic loop
|
|
470
|
+
* has the same conditions to report, and a second copy is how two loops
|
|
471
|
+
* come to emit subtly different payloads for the same event name.
|
|
472
|
+
*/
|
|
473
|
+
protected emitTurnEvent(payload: {
|
|
474
|
+
phase: "step-cap" | "context-cap" | "time-limit" | "stalled" | "aborted" | "provider-error" | "tool-timeout" | "malformed-retry";
|
|
475
|
+
step?: number;
|
|
476
|
+
maxSteps?: number;
|
|
477
|
+
toolName?: string;
|
|
478
|
+
toolCallCount?: number;
|
|
479
|
+
elapsedMs?: number;
|
|
480
|
+
}): void;
|
|
463
481
|
/**
|
|
464
482
|
* The per-provider generate budget from the descriptor, for native paths
|
|
465
483
|
* that call `withTurnTimeout` directly.
|
|
@@ -2014,6 +2014,28 @@ export class BaseProvider {
|
|
|
2014
2014
|
timeoutController?.cleanup();
|
|
2015
2015
|
}
|
|
2016
2016
|
}
|
|
2017
|
+
/**
|
|
2018
|
+
* Emit a `turn:lifecycle` event on the SDK emitter (alongside
|
|
2019
|
+
* tool:start/tool:end) so loop conditions that previously only reached
|
|
2020
|
+
* process logs — step-cap, time-limit, stall, abort, tool timeouts,
|
|
2021
|
+
* malformed-call retries — are observable by consumers' own pipelines.
|
|
2022
|
+
*
|
|
2023
|
+
* Lives here rather than on one provider because every native agentic loop
|
|
2024
|
+
* has the same conditions to report, and a second copy is how two loops
|
|
2025
|
+
* come to emit subtly different payloads for the same event name.
|
|
2026
|
+
*/
|
|
2027
|
+
emitTurnEvent(payload) {
|
|
2028
|
+
try {
|
|
2029
|
+
this.neurolink?.getEventEmitter()?.emit("turn:lifecycle", {
|
|
2030
|
+
provider: this.providerName,
|
|
2031
|
+
timestamp: Date.now(),
|
|
2032
|
+
...payload,
|
|
2033
|
+
});
|
|
2034
|
+
}
|
|
2035
|
+
catch {
|
|
2036
|
+
/* listener errors are non-fatal */
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
2017
2039
|
/**
|
|
2018
2040
|
* The per-provider generate budget from the descriptor, for native paths
|
|
2019
2041
|
* that call `withTurnTimeout` directly.
|
|
@@ -163,9 +163,17 @@ export class MessageBuilder {
|
|
|
163
163
|
};
|
|
164
164
|
}
|
|
165
165
|
else if (item.type === "image") {
|
|
166
|
+
// Upstream image builders label the resolved format as
|
|
167
|
+
// either `mediaType` (canonical ImagePart field) or
|
|
168
|
+
// `mimeType` (messageBuilder's convertSimpleImagesToProviderFormat).
|
|
169
|
+
// Dropping it here silently reset every transcoded image
|
|
170
|
+
// back to the provider's hardcoded PNG default even
|
|
171
|
+
// though the transcoded bytes themselves were correct.
|
|
172
|
+
const mediaType = (item.mediaType ?? item.mimeType);
|
|
166
173
|
return {
|
|
167
174
|
type: "image",
|
|
168
175
|
image: item.image || "",
|
|
176
|
+
...(mediaType && { mediaType }),
|
|
169
177
|
...(itemProviderOptions && {
|
|
170
178
|
providerOptions: itemProviderOptions,
|
|
171
179
|
}),
|
|
@@ -287,9 +295,17 @@ export class MessageBuilder {
|
|
|
287
295
|
};
|
|
288
296
|
}
|
|
289
297
|
else if (item.type === "image") {
|
|
298
|
+
// Upstream image builders label the resolved format as
|
|
299
|
+
// either `mediaType` (canonical ImagePart field) or
|
|
300
|
+
// `mimeType` (messageBuilder's convertSimpleImagesToProviderFormat).
|
|
301
|
+
// Dropping it here silently reset every transcoded image
|
|
302
|
+
// back to the provider's hardcoded PNG default even
|
|
303
|
+
// though the transcoded bytes themselves were correct.
|
|
304
|
+
const mediaType = (item.mediaType ?? item.mimeType);
|
|
290
305
|
return {
|
|
291
306
|
type: "image",
|
|
292
307
|
image: item.image || "",
|
|
308
|
+
...(mediaType && { mediaType }),
|
|
293
309
|
...(itemProviderOptions && {
|
|
294
310
|
providerOptions: itemProviderOptions,
|
|
295
311
|
}),
|
|
@@ -43,6 +43,26 @@ export declare class ProviderFactory {
|
|
|
43
43
|
/**
|
|
44
44
|
* Get list of available providers
|
|
45
45
|
*/
|
|
46
|
+
/**
|
|
47
|
+
* Explain why a provider name did not resolve, usefully (#354).
|
|
48
|
+
*
|
|
49
|
+
* Three distinct situations used to share one generic message:
|
|
50
|
+
*
|
|
51
|
+
* - The registry is empty, because `ProviderRegistry.registerAllProviders()`
|
|
52
|
+
* has not run yet. Every name is unresolvable, and listing zero available
|
|
53
|
+
* providers tells the reader nothing about why.
|
|
54
|
+
* - The name IS a known provider but has no registration — recognised,
|
|
55
|
+
* unavailable. Distinct from a typo and worth saying so.
|
|
56
|
+
* - The name is not a provider at all, in which case a near-miss is almost
|
|
57
|
+
* always a typo and is worth naming: "opennai" -> "openai".
|
|
58
|
+
*
|
|
59
|
+
* Canonical names are offered as suggestions ahead of aliases, so a typo
|
|
60
|
+
* resolves to the name the docs use.
|
|
61
|
+
*
|
|
62
|
+
* @param requested - The provider name as the caller supplied it
|
|
63
|
+
* @returns A message naming the situation and what to do about it
|
|
64
|
+
*/
|
|
65
|
+
private static describeUnresolvableProvider;
|
|
46
66
|
static getAvailableProviders(): string[];
|
|
47
67
|
/**
|
|
48
68
|
* Get provider registration info
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { logger } from "../utils/logger.js";
|
|
2
|
+
import { suggestClosest } from "../utils/stringDistance.js";
|
|
2
3
|
import { PROVIDER_DESCRIPTORS, PROVIDER_DESCRIPTORS_BY_NAME, PROVIDER_ALIAS_INDEX, } from "./providerDescriptors.js";
|
|
3
4
|
// Pure factory pattern with no hardcoded imports
|
|
4
5
|
// All providers loaded dynamically via registry to avoid circular dependencies
|
|
@@ -63,28 +64,52 @@ export class ProviderFactory {
|
|
|
63
64
|
const normalizedName = resolvedProviderName.toLowerCase();
|
|
64
65
|
const registration = ProviderFactory.providers.get(normalizedName);
|
|
65
66
|
if (!registration) {
|
|
66
|
-
throw new Error(
|
|
67
|
+
throw new Error(ProviderFactory.describeUnresolvableProvider(resolvedProviderName));
|
|
67
68
|
}
|
|
68
|
-
//
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
69
|
+
// Everything below this point — model resolution included — is wrapped in
|
|
70
|
+
// the same try/catch. The dynamic import a few lines down is on this
|
|
71
|
+
// path too: a load failure there is a provider-construction failure like
|
|
72
|
+
// any other and must come back as "Failed to create provider ...", not
|
|
73
|
+
// escape unwrapped.
|
|
74
|
+
try {
|
|
75
|
+
// Respect environment variables before falling back to registry default
|
|
76
|
+
let model = modelName;
|
|
77
|
+
if (model) {
|
|
78
|
+
// #337: the model registry has always carried aliases ("gpt4o" ->
|
|
79
|
+
// "gpt-4o"), and capability lookups resolve through them, but the id
|
|
80
|
+
// actually sent to the provider did not — so `generate({ model:
|
|
81
|
+
// "gpt4o" })` put the alias on the wire and the provider 404'd.
|
|
82
|
+
// Imported lazily: the registry is a large module and this is the only
|
|
83
|
+
// thing on the construction path that needs it.
|
|
84
|
+
const { resolveProviderModelAlias } = await import("../models/modelRegistry.js");
|
|
85
|
+
const canonicalProvider = ProviderFactory.getDescriptor(normalizedName)?.name ?? normalizedName;
|
|
86
|
+
const canonicalModel = resolveProviderModelAlias(canonicalProvider, model);
|
|
87
|
+
if (canonicalModel) {
|
|
88
|
+
logger.debug("[ProviderFactory] Resolved model alias", {
|
|
89
|
+
provider: canonicalProvider,
|
|
90
|
+
requested: model,
|
|
91
|
+
resolved: canonicalModel,
|
|
92
|
+
});
|
|
93
|
+
model = canonicalModel;
|
|
94
|
+
}
|
|
75
95
|
}
|
|
76
|
-
|
|
77
|
-
|
|
96
|
+
if (!model) {
|
|
97
|
+
// Check for provider-specific environment variables
|
|
98
|
+
if (resolvedProviderName.toLowerCase().includes("vertex")) {
|
|
99
|
+
// Use gemini-2.5-flash as default - latest GA model with best price-performance
|
|
100
|
+
model = process.env.VERTEX_MODEL || "gemini-2.5-flash";
|
|
101
|
+
}
|
|
102
|
+
else if (resolvedProviderName.toLowerCase().includes("bedrock")) {
|
|
103
|
+
model = process.env.BEDROCK_MODEL || process.env.BEDROCK_MODEL_ID;
|
|
104
|
+
}
|
|
105
|
+
// Fallback to registry default if no env var
|
|
106
|
+
model = model || registration.defaultModel;
|
|
78
107
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const scopedCredentials = credentials
|
|
85
|
-
? credentials[credKey]
|
|
86
|
-
: undefined;
|
|
87
|
-
try {
|
|
108
|
+
const credKey = resolveCredentialKey(normalizedName);
|
|
109
|
+
// Extract provider-scoped credential slice (e.g. credentials.openai for OpenAI)
|
|
110
|
+
const scopedCredentials = credentials
|
|
111
|
+
? credentials[credKey]
|
|
112
|
+
: undefined;
|
|
88
113
|
if (typeof registration.constructor !== "function") {
|
|
89
114
|
throw new Error(`Invalid constructor for provider ${providerName}: not a function`);
|
|
90
115
|
}
|
|
@@ -106,6 +131,62 @@ export class ProviderFactory {
|
|
|
106
131
|
/**
|
|
107
132
|
* Get list of available providers
|
|
108
133
|
*/
|
|
134
|
+
/**
|
|
135
|
+
* Explain why a provider name did not resolve, usefully (#354).
|
|
136
|
+
*
|
|
137
|
+
* Three distinct situations used to share one generic message:
|
|
138
|
+
*
|
|
139
|
+
* - The registry is empty, because `ProviderRegistry.registerAllProviders()`
|
|
140
|
+
* has not run yet. Every name is unresolvable, and listing zero available
|
|
141
|
+
* providers tells the reader nothing about why.
|
|
142
|
+
* - The name IS a known provider but has no registration — recognised,
|
|
143
|
+
* unavailable. Distinct from a typo and worth saying so.
|
|
144
|
+
* - The name is not a provider at all, in which case a near-miss is almost
|
|
145
|
+
* always a typo and is worth naming: "opennai" -> "openai".
|
|
146
|
+
*
|
|
147
|
+
* Canonical names are offered as suggestions ahead of aliases, so a typo
|
|
148
|
+
* resolves to the name the docs use.
|
|
149
|
+
*
|
|
150
|
+
* @param requested - The provider name as the caller supplied it
|
|
151
|
+
* @returns A message naming the situation and what to do about it
|
|
152
|
+
*/
|
|
153
|
+
static describeUnresolvableProvider(requested) {
|
|
154
|
+
const registered = ProviderFactory.getAvailableProviders();
|
|
155
|
+
if (registered.length === 0) {
|
|
156
|
+
return (`Cannot resolve provider "${requested}": no providers are registered yet. ` +
|
|
157
|
+
`ProviderRegistry.registerAllProviders() has not completed, so every name ` +
|
|
158
|
+
`is unresolvable at this point — this is a lifecycle problem, not a bad name.`);
|
|
159
|
+
}
|
|
160
|
+
// `registered` mixes canonical names and aliases (registerProvider sets
|
|
161
|
+
// both as separate map keys — see registerProvider above), and a name
|
|
162
|
+
// registered without a descriptor (a caller-supplied registerProvider()
|
|
163
|
+
// with no 5th argument) has no entry in PROVIDER_ALIAS_INDEX at all. Only
|
|
164
|
+
// PROVIDER_DESCRIPTORS would previously stand in for "what's registered",
|
|
165
|
+
// which listed every built-in provider whether or not this factory
|
|
166
|
+
// instance had actually registered it, and mislabelled a descriptor-less
|
|
167
|
+
// custom registration as an "alias" simply for not matching that static
|
|
168
|
+
// list. Deriving both lists from `registered` itself fixes both: a
|
|
169
|
+
// registered name canonicalizes to what PROVIDER_ALIAS_INDEX resolves it
|
|
170
|
+
// to (itself, when it has no descriptor), and only a name that resolves
|
|
171
|
+
// to a *different*, registered canonical name is a true alias.
|
|
172
|
+
const canonical = Array.from(new Set(registered.map((name) => PROVIDER_ALIAS_INDEX.get(name) ?? name))).sort();
|
|
173
|
+
const aliases = registered
|
|
174
|
+
.filter((name) => {
|
|
175
|
+
const target = PROVIDER_ALIAS_INDEX.get(name);
|
|
176
|
+
return target !== undefined && target !== name;
|
|
177
|
+
})
|
|
178
|
+
.sort();
|
|
179
|
+
const known = PROVIDER_ALIAS_INDEX.get(requested.toLowerCase());
|
|
180
|
+
if (known) {
|
|
181
|
+
return (`Provider "${requested}" is a recognised provider (${known}) but is not registered ` +
|
|
182
|
+
`in this factory, so it cannot be constructed. Registered providers: ${canonical.join(", ")}.`);
|
|
183
|
+
}
|
|
184
|
+
// Canonical names first so a tie resolves to the documented spelling.
|
|
185
|
+
const suggestions = suggestClosest(requested, [...canonical, ...aliases]);
|
|
186
|
+
const didYouMean = suggestions.length > 0 ? ` Did you mean: ${suggestions.join(", ")}?` : "";
|
|
187
|
+
return (`Unknown provider: "${requested}".${didYouMean} ` +
|
|
188
|
+
`Valid providers: ${canonical.join(", ")}. Accepted aliases: ${aliases.join(", ")}.`);
|
|
189
|
+
}
|
|
109
190
|
static getAvailableProviders() {
|
|
110
191
|
return Array.from(ProviderFactory.providers.keys()).filter((name, index, arr) => arr.indexOf(name) === index);
|
|
111
192
|
}
|
|
@@ -34,6 +34,31 @@ export declare function getAllModels(): ModelInfo[];
|
|
|
34
34
|
* Get model by ID
|
|
35
35
|
*/
|
|
36
36
|
export declare function getModelById(id: string): ModelInfo | undefined;
|
|
37
|
+
/**
|
|
38
|
+
* Resolve a registry alias to the canonical model id for a given provider.
|
|
39
|
+
*
|
|
40
|
+
* The registry has carried per-model aliases ("gpt4o" -> "gpt-4o", "gpt4" ->
|
|
41
|
+
* "gpt-4") all along, and capability lookups like {@link modelSupports}
|
|
42
|
+
* already resolve through them — but the id actually sent to the provider did
|
|
43
|
+
* not, so `generate({ model: "gpt4o" })` put the alias on the wire and the
|
|
44
|
+
* provider 404'd. This closes that gap (#337).
|
|
45
|
+
*
|
|
46
|
+
* Deliberately conservative, because a wrong rewrite is worse than no rewrite:
|
|
47
|
+
*
|
|
48
|
+
* - An id the registry already knows is returned untouched, so a canonical id
|
|
49
|
+
* can never be displaced by an alias that happens to collide with it.
|
|
50
|
+
* - An alias is only applied when the model it names belongs to the provider
|
|
51
|
+
* being constructed. Without that check, an aggregator or self-hosted
|
|
52
|
+
* deployment whose model is literally named "gpt4" would be silently
|
|
53
|
+
* rewritten to OpenAI's "gpt-4".
|
|
54
|
+
* - Anything unrecognised passes through, so custom and brand-new model ids
|
|
55
|
+
* keep working exactly as before.
|
|
56
|
+
*
|
|
57
|
+
* @param provider - Provider being constructed (canonical name)
|
|
58
|
+
* @param model - Model id or alias as the caller supplied it
|
|
59
|
+
* @returns The canonical id when an alias resolved, otherwise null
|
|
60
|
+
*/
|
|
61
|
+
export declare function resolveProviderModelAlias(provider: string, model: string): string | null;
|
|
37
62
|
/**
|
|
38
63
|
* Check whether a model supports a registered capability.
|
|
39
64
|
*
|
|
@@ -2689,6 +2689,45 @@ export function getAllModels() {
|
|
|
2689
2689
|
export function getModelById(id) {
|
|
2690
2690
|
return MODEL_REGISTRY[id];
|
|
2691
2691
|
}
|
|
2692
|
+
/**
|
|
2693
|
+
* Resolve a registry alias to the canonical model id for a given provider.
|
|
2694
|
+
*
|
|
2695
|
+
* The registry has carried per-model aliases ("gpt4o" -> "gpt-4o", "gpt4" ->
|
|
2696
|
+
* "gpt-4") all along, and capability lookups like {@link modelSupports}
|
|
2697
|
+
* already resolve through them — but the id actually sent to the provider did
|
|
2698
|
+
* not, so `generate({ model: "gpt4o" })` put the alias on the wire and the
|
|
2699
|
+
* provider 404'd. This closes that gap (#337).
|
|
2700
|
+
*
|
|
2701
|
+
* Deliberately conservative, because a wrong rewrite is worse than no rewrite:
|
|
2702
|
+
*
|
|
2703
|
+
* - An id the registry already knows is returned untouched, so a canonical id
|
|
2704
|
+
* can never be displaced by an alias that happens to collide with it.
|
|
2705
|
+
* - An alias is only applied when the model it names belongs to the provider
|
|
2706
|
+
* being constructed. Without that check, an aggregator or self-hosted
|
|
2707
|
+
* deployment whose model is literally named "gpt4" would be silently
|
|
2708
|
+
* rewritten to OpenAI's "gpt-4".
|
|
2709
|
+
* - Anything unrecognised passes through, so custom and brand-new model ids
|
|
2710
|
+
* keep working exactly as before.
|
|
2711
|
+
*
|
|
2712
|
+
* @param provider - Provider being constructed (canonical name)
|
|
2713
|
+
* @param model - Model id or alias as the caller supplied it
|
|
2714
|
+
* @returns The canonical id when an alias resolved, otherwise null
|
|
2715
|
+
*/
|
|
2716
|
+
export function resolveProviderModelAlias(provider, model) {
|
|
2717
|
+
const normalizedModel = model.toLowerCase();
|
|
2718
|
+
if (MODEL_REGISTRY[model] || MODEL_REGISTRY[normalizedModel]) {
|
|
2719
|
+
return null;
|
|
2720
|
+
}
|
|
2721
|
+
const aliasTarget = MODEL_ALIASES[normalizedModel];
|
|
2722
|
+
if (!aliasTarget) {
|
|
2723
|
+
return null;
|
|
2724
|
+
}
|
|
2725
|
+
const target = MODEL_REGISTRY[aliasTarget];
|
|
2726
|
+
if (!target || target.provider !== provider.toLowerCase()) {
|
|
2727
|
+
return null;
|
|
2728
|
+
}
|
|
2729
|
+
return target.id;
|
|
2730
|
+
}
|
|
2692
2731
|
/**
|
|
2693
2732
|
* Check whether a model supports a registered capability.
|
|
2694
2733
|
*
|