@juspay/neurolink 12.21.2 → 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.
@@ -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.
@@ -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(`Unknown provider: ${resolvedProviderName}. Available providers: ${ProviderFactory.getAvailableProviders().join(", ")}`);
67
+ throw new Error(ProviderFactory.describeUnresolvableProvider(resolvedProviderName));
67
68
  }
68
- // Respect environment variables before falling back to registry default
69
- let model = modelName;
70
- if (!model) {
71
- // Check for provider-specific environment variables
72
- if (resolvedProviderName.toLowerCase().includes("vertex")) {
73
- // Use gemini-2.5-flash as default - latest GA model with best price-performance
74
- model = process.env.VERTEX_MODEL || "gemini-2.5-flash";
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
- else if (resolvedProviderName.toLowerCase().includes("bedrock")) {
77
- model = process.env.BEDROCK_MODEL || process.env.BEDROCK_MODEL_ID;
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
- // Fallback to registry default if no env var
80
- model = model || registration.defaultModel;
81
- }
82
- const credKey = resolveCredentialKey(normalizedName);
83
- // Extract provider-scoped credential slice (e.g. credentials.openai for OpenAI)
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
  *