@juspay/neurolink 12.0.1 ā 12.0.2
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 +2 -2
- package/dist/browser/neurolink.min.js +270 -270
- package/dist/cli/commands/models.js +42 -0
- package/dist/core/baseProvider.js +13 -3
- package/dist/core/modules/GenerationHandler.js +5 -1
- package/dist/neurolink.js +17 -2
- package/dist/providers/litellm/client.js +10 -2
- package/dist/utils/providerErrorClassification.js +5 -1
- package/dist/utils/providerHealth.js +30 -6
- package/package.json +1 -1
|
@@ -320,6 +320,48 @@ export class ModelsCommandFactory {
|
|
|
320
320
|
if (!argv.deprecated) {
|
|
321
321
|
models = models.filter((model) => !model.deprecated);
|
|
322
322
|
}
|
|
323
|
+
// Live-discovery fallback: providers that resolve their catalog at
|
|
324
|
+
// runtime (litellm, ollama, openai-compatible gateways) have no static
|
|
325
|
+
// registry rows, so a scoped `models list --provider litellm` printed
|
|
326
|
+
// "Found 0 models" while generate/stream against those models worked.
|
|
327
|
+
// When the static registry is empty for a single requested provider,
|
|
328
|
+
// ask the provider itself.
|
|
329
|
+
if (models.length === 0 &&
|
|
330
|
+
argv.provider &&
|
|
331
|
+
(Array.isArray(argv.provider) ? argv.provider : [argv.provider])
|
|
332
|
+
.length === 1) {
|
|
333
|
+
const providerName = String(Array.isArray(argv.provider) ? argv.provider[0] : argv.provider);
|
|
334
|
+
try {
|
|
335
|
+
const { AIProviderFactory } = await import("../../core/factory.js");
|
|
336
|
+
const provider = (await AIProviderFactory.createProvider(providerName));
|
|
337
|
+
const liveIds = typeof provider.getAvailableModels === "function"
|
|
338
|
+
? await provider.getAvailableModels()
|
|
339
|
+
: [];
|
|
340
|
+
if (liveIds.length > 0) {
|
|
341
|
+
if (spinner) {
|
|
342
|
+
spinner.succeed(`Found ${liveIds.length} models (live from ${providerName})`);
|
|
343
|
+
}
|
|
344
|
+
if (argv.format === "json") {
|
|
345
|
+
logger.always(JSON.stringify(liveIds.map((id) => ({
|
|
346
|
+
id,
|
|
347
|
+
provider: providerName,
|
|
348
|
+
source: "live",
|
|
349
|
+
})), null, 2));
|
|
350
|
+
}
|
|
351
|
+
else {
|
|
352
|
+
logger.always(chalk.bold(`\nš Models exposed by ${providerName} (live discovery):\n`));
|
|
353
|
+
for (const id of liveIds) {
|
|
354
|
+
logger.always(` ${id}`);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
catch {
|
|
361
|
+
// Live discovery is best-effort ā fall through to the static
|
|
362
|
+
// (empty) listing rather than turning a listing into an error.
|
|
363
|
+
}
|
|
364
|
+
}
|
|
323
365
|
if (spinner) {
|
|
324
366
|
spinner.succeed(`Found ${models.length} models`);
|
|
325
367
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { context, SpanKind, SpanStatusCode, trace } from "@opentelemetry/api";
|
|
2
2
|
import { directAgentTools } from "../agent/directTools.js";
|
|
3
3
|
import { defaultProviderFor } from "../factories/mediaHandlerCatalog.js";
|
|
4
|
+
import { PROVIDER_DESCRIPTORS_BY_NAME } from "../factories/providerDescriptors.js";
|
|
4
5
|
import { MiddlewareFactory } from "../middleware/factory.js";
|
|
5
6
|
import { modelSupports } from "../models/modelRegistry.js";
|
|
6
7
|
import { resolveRequestKind } from "./resolveRequestKind.js";
|
|
@@ -1243,14 +1244,23 @@ export class BaseProvider {
|
|
|
1243
1244
|
}, options, startTime);
|
|
1244
1245
|
}
|
|
1245
1246
|
async executeStandardGenerateFlow(options, startTime, model, messages, tools) {
|
|
1246
|
-
// Apply a defensive default timeout
|
|
1247
|
-
//
|
|
1247
|
+
// Apply a defensive default timeout when the caller didn't pass one.
|
|
1248
|
+
// Without this guard, AI SDK's generateText() will wait forever on
|
|
1248
1249
|
// an upstream that accepts the connection but never produces a response
|
|
1249
1250
|
// (observed against the litellm gateway when a request triggers the
|
|
1250
1251
|
// team-access denial path ā connection stays open, no response is sent,
|
|
1251
1252
|
// and the matrix test hangs the entire suite). Callers can still pass
|
|
1252
1253
|
// a larger value (e.g. video generation passes 10 min).
|
|
1253
|
-
|
|
1254
|
+
//
|
|
1255
|
+
// A provider descriptor may declare a LARGER generate budget than the
|
|
1256
|
+
// 3-min floor (litellm: 300s ā slow proxied models routinely need more
|
|
1257
|
+
// than 180s end-to-end even while streaming). The declared value only
|
|
1258
|
+
// ever raises the default, never lowers it: several descriptors carry
|
|
1259
|
+
// aspirational sub-180s numbers (openai 30s, bedrock 45s) that were
|
|
1260
|
+
// never enforced on this path, and enforcing them now would break
|
|
1261
|
+
// long-running generations that have always been allowed.
|
|
1262
|
+
const descriptorGenerateMs = PROVIDER_DESCRIPTORS_BY_NAME.get(this.providerName)?.timeouts?.generateMs;
|
|
1263
|
+
const effectiveTimeout = options.timeout ?? Math.max(descriptorGenerateMs ?? 0, 180_000);
|
|
1254
1264
|
const timeoutController = createTimeoutController(effectiveTimeout, this.providerName, "generate");
|
|
1255
1265
|
const composedSignal = composeAbortSignals(options.abortSignal, timeoutController?.controller.signal);
|
|
1256
1266
|
const composedOptions = composedSignal
|
|
@@ -731,7 +731,11 @@ export class GenerationHandler {
|
|
|
731
731
|
toolName;
|
|
732
732
|
toolsUsed.push(toolName);
|
|
733
733
|
let callArgs = {};
|
|
734
|
-
if (tcRecord.
|
|
734
|
+
if (tcRecord.input) {
|
|
735
|
+
// AI SDK v6 carries tool-call arguments as `input`.
|
|
736
|
+
callArgs = tcRecord.input;
|
|
737
|
+
}
|
|
738
|
+
else if (tcRecord.args) {
|
|
735
739
|
callArgs = tcRecord.args;
|
|
736
740
|
}
|
|
737
741
|
else if (tcRecord.arguments) {
|
package/dist/neurolink.js
CHANGED
|
@@ -102,7 +102,7 @@ import { getWorkflow } from "./workflow/core/workflowRegistry.js";
|
|
|
102
102
|
import { runWorkflow } from "./workflow/core/workflowRunner.js";
|
|
103
103
|
import { ModelPool, classifyProviderError } from "./routing/index.js";
|
|
104
104
|
import { ClassifierRouter } from "./routing/classifierRouter.js";
|
|
105
|
-
import { looksLikeModelAccessDenied as sharedLooksLikeModelAccessDenied, isNonRetryableProviderError as sharedIsNonRetryableProviderError, isNonRetryableForPool as sharedIsNonRetryableForPool, } from "./utils/providerErrorClassification.js";
|
|
105
|
+
import { looksLikeModelAccessDenied as sharedLooksLikeModelAccessDenied, looksLikeModelNotFound as sharedLooksLikeModelNotFound, isNonRetryableProviderError as sharedIsNonRetryableProviderError, isNonRetryableForPool as sharedIsNonRetryableForPool, } from "./utils/providerErrorClassification.js";
|
|
106
106
|
import { getErrorStatusCode } from "./utils/providerRetry.js";
|
|
107
107
|
import { detectAndRedactPII } from "./utils/piiDetector.js";
|
|
108
108
|
import { validateResponse } from "./utils/responseValidator.js";
|
|
@@ -3211,6 +3211,16 @@ Current user's request: ${currentInput}`;
|
|
|
3211
3211
|
// String prompts are immutable, so they pass through.
|
|
3212
3212
|
if (typeof optionsOrPrompt !== "string") {
|
|
3213
3213
|
optionsOrPrompt = cloneOptionsForCallIsolation(optionsOrPrompt);
|
|
3214
|
+
// The deprecated `conversationHistory` field is not wired into message
|
|
3215
|
+
// building ā messages passed there never reach the model, which reads
|
|
3216
|
+
// as "the SDK forgot my context" rather than a caller bug. Warn loudly
|
|
3217
|
+
// instead of failing silently; `conversationMessages` is the wired path.
|
|
3218
|
+
const legacyOpts = optionsOrPrompt;
|
|
3219
|
+
if (Array.isArray(legacyOpts.conversationHistory) &&
|
|
3220
|
+
legacyOpts.conversationHistory.length > 0 &&
|
|
3221
|
+
!legacyOpts.conversationMessages) {
|
|
3222
|
+
logger.warn("[NeuroLink.generate] `conversationHistory` is deprecated and NOT passed to the model ā use `conversationMessages` (ChatMessage[]) instead.");
|
|
3223
|
+
}
|
|
3214
3224
|
}
|
|
3215
3225
|
// Retrieve once at the public call boundary so fallback attempts reuse the
|
|
3216
3226
|
// same grounding block and internal preparation cannot inject it twice.
|
|
@@ -3347,9 +3357,14 @@ Current user's request: ${currentInput}`;
|
|
|
3347
3357
|
// mid-fallback; cloneOptionsForCallIsolation keeps abortSignal
|
|
3348
3358
|
// by-reference, so the retried options observe the same signal.
|
|
3349
3359
|
const callerAborted = () => callOpts.abortSignal?.aborted === true;
|
|
3360
|
+
// modelChain-only orchestration advances on two error shapes the next
|
|
3361
|
+
// member can actually fix: access-denied (the original gate) and
|
|
3362
|
+
// model-not-found ("Invalid model name" from a gateway that doesn't
|
|
3363
|
+
// serve that member ā a chain written for one LiteLLM deployment must
|
|
3364
|
+
// not hard-fail on another just because a member is absent there).
|
|
3350
3365
|
const shouldOrchestrateFallback = (err) => effectiveCallback
|
|
3351
3366
|
? !(isAbortError(err) && callerAborted())
|
|
3352
|
-
: looksLikeModelAccessDenied(err);
|
|
3367
|
+
: looksLikeModelAccessDenied(err) || sharedLooksLikeModelNotFound(err);
|
|
3353
3368
|
if (!shouldOrchestrateFallback(lastError)) {
|
|
3354
3369
|
throw lastError;
|
|
3355
3370
|
}
|
|
@@ -367,8 +367,11 @@ export class LiteLLMProvider extends OpenAIChatCompletionsProvider {
|
|
|
367
367
|
{
|
|
368
368
|
match: (ctx) => /ECONNREFUSED|Failed to fetch/.test(ctx.message),
|
|
369
369
|
errorClass: NetworkError,
|
|
370
|
+
// Name the base URL this instance actually dialed ā per-request
|
|
371
|
+
// credentials can override LITELLM_BASE_URL, and an error pointing
|
|
372
|
+
// at the env value sends the caller debugging the wrong host.
|
|
370
373
|
message: () => "LiteLLM proxy server not available. Please start the LiteLLM proxy server at " +
|
|
371
|
-
|
|
374
|
+
redactUrlCredentials(this.config.baseURL),
|
|
372
375
|
},
|
|
373
376
|
{
|
|
374
377
|
match: (ctx) => /API_KEY_INVALID|Invalid API key/.test(ctx.message),
|
|
@@ -420,7 +423,12 @@ export class LiteLLMProvider extends OpenAIChatCompletionsProvider {
|
|
|
420
423
|
return this.getFallbackModels();
|
|
421
424
|
}
|
|
422
425
|
async fetchModelsFromAPI() {
|
|
423
|
-
|
|
426
|
+
// Tolerate a `/v1`-suffixed base URL. Chat appends /chat/completions, so
|
|
427
|
+
// deployments are commonly configured with base = https://host/v1 ā but
|
|
428
|
+
// appending /v1/models to that yields /v1/v1/models, which LiteLLM 404s,
|
|
429
|
+
// and discovery silently degrades to the hardcoded fallback list.
|
|
430
|
+
const root = stripTrailingSlash(this.config.baseURL).replace(/\/v1$/, "");
|
|
431
|
+
const modelsUrl = `${root}/v1/models`;
|
|
424
432
|
const proxyFetch = createProxyFetch();
|
|
425
433
|
const controller = new AbortController();
|
|
426
434
|
const timeoutId = setTimeout(() => controller.abort(), 5000);
|
|
@@ -67,7 +67,11 @@ export function looksLikeModelNotFound(error) {
|
|
|
67
67
|
lower.includes("not_found_error") ||
|
|
68
68
|
msg.includes("NOT_FOUND") ||
|
|
69
69
|
lower.includes("does not exist") ||
|
|
70
|
-
lower.includes("unknown model")
|
|
70
|
+
lower.includes("unknown model") ||
|
|
71
|
+
// LiteLLM's phrasing for a model id its router doesn't serve ā a 400,
|
|
72
|
+
// not a 404: "Invalid model name passed in model=... Call `/v1/models`
|
|
73
|
+
// to view available models for your key."
|
|
74
|
+
lower.includes("invalid model name")) &&
|
|
71
75
|
namesAModel);
|
|
72
76
|
}
|
|
73
77
|
/**
|
|
@@ -808,13 +808,37 @@ export class ProviderHealthChecker {
|
|
|
808
808
|
healthStatus.recommendations.push("Set LITELLM_BASE_URL to a valid URL (e.g., http://localhost:4000)");
|
|
809
809
|
return;
|
|
810
810
|
}
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
811
|
+
// Only pin the availability check to a specific model when the user
|
|
812
|
+
// explicitly configured one. The fallback default ("openai/gpt-4o-mini")
|
|
813
|
+
// is a guess, not configuration ā proxies that serve a different model
|
|
814
|
+
// set (every self-hosted gateway) were reported "Not configured" here
|
|
815
|
+
// while generate/stream against them worked fine with explicit models.
|
|
816
|
+
const configuredModel = process.env.LITELLM_MODEL;
|
|
817
|
+
let failureReason;
|
|
818
|
+
if (configuredModel) {
|
|
819
|
+
const availability = await this.checkLiteLLMAvailability({
|
|
820
|
+
model: configuredModel,
|
|
821
|
+
timeout,
|
|
822
|
+
});
|
|
823
|
+
if (!availability.available) {
|
|
824
|
+
failureReason = availability.reason ?? "unknown error";
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
else {
|
|
828
|
+
// No configured model: a reachable proxy with any models counts.
|
|
829
|
+
try {
|
|
830
|
+
const models = await this.getLiteLLMAvailableModels(timeout);
|
|
831
|
+
if (models.length === 0) {
|
|
832
|
+
failureReason = "LiteLLM returned an empty model list";
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
catch (error) {
|
|
836
|
+
failureReason = error instanceof Error ? error.message : String(error);
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
if (failureReason !== undefined) {
|
|
816
840
|
healthStatus.isConfigured = false;
|
|
817
|
-
healthStatus.configurationIssues.push(`LiteLLM runtime check failed: ${
|
|
841
|
+
healthStatus.configurationIssues.push(`LiteLLM runtime check failed: ${failureReason}`);
|
|
818
842
|
healthStatus.recommendations.push("Start the LiteLLM proxy and ensure the configured model is available from /v1/models");
|
|
819
843
|
return;
|
|
820
844
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juspay/neurolink",
|
|
3
|
-
"version": "12.0.
|
|
3
|
+
"version": "12.0.2",
|
|
4
4
|
"packageManager": "pnpm@10.15.1",
|
|
5
5
|
"description": "TypeScript AI SDK with 24+ LLM providers behind one consistent API. MCP-native (connect any MCP server), voice TTS/STT/realtime, RAG, agents, memory, context compaction. OpenAI Ā· Anthropic Ā· Gemini Ā· Bedrock Ā· Azure Ā· Ollama Ā· DeepSeek Ā· NVIDIA NIM and more.",
|
|
6
6
|
"author": {
|