@juspay/neurolink 12.25.5 → 12.26.1
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 +382 -382
- package/dist/cli/commands/decide.d.ts +2 -1
- package/dist/cli/commands/decide.js +4 -2
- package/dist/cli/commands/setup.js +2 -1
- package/dist/cli/factories/commandFactory.js +1 -1
- package/dist/cli/utils/classifierRouterFlags.js +1 -1
- package/dist/constants/enums.d.ts +15 -0
- package/dist/constants/enums.js +16 -0
- package/dist/core/baseProvider.js +2 -2
- package/dist/factories/providerDescriptors.d.ts +16 -7
- package/dist/factories/providerDescriptors.js +97 -15
- package/dist/factories/providerRegistry.js +9 -1
- package/dist/mcp/externalServerManager.js +16 -2
- package/dist/mcp/mcpCircuitBreaker.d.ts +24 -3
- package/dist/mcp/mcpCircuitBreaker.js +57 -18
- package/dist/mcp/toolDiscoveryService.js +20 -4
- package/dist/models/manifestRegistry.js +2 -0
- package/dist/models/manifests/laya.d.ts +12 -0
- package/dist/models/manifests/laya.js +54 -0
- package/dist/neurolink.js +8 -3
- package/dist/providers/laya.d.ts +40 -0
- package/dist/providers/laya.js +170 -0
- package/dist/providers/systemOneDecision.d.ts +102 -0
- package/dist/providers/systemOneDecision.js +443 -0
- package/dist/providers/typesafe.d.ts +32 -47
- package/dist/providers/typesafe.js +64 -386
- package/dist/routing/classifierRouter.d.ts +2 -2
- package/dist/routing/classifierRouter.js +10 -8
- package/dist/types/classifierRouter.d.ts +13 -3
- package/dist/types/cli.d.ts +3 -2
- package/dist/types/decision.d.ts +29 -0
- package/dist/types/externalMcp.d.ts +7 -0
- package/dist/types/providers.d.ts +20 -1
- package/dist/utils/modelChoices.js +21 -1
- package/dist/utils/providerConfig.d.ts +5 -0
- package/dist/utils/providerConfig.js +17 -0
- package/docs-site/static/search-index.json +24 -10
- package/package.json +4 -2
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
* for the full vocabulary.
|
|
9
9
|
*
|
|
10
10
|
* Credentials are env-only here, exactly as every other CLI command: the
|
|
11
|
-
* decision provider
|
|
11
|
+
* decision provider reads its own settings (TYPESAFE_API_KEY, or LAYA_API_KEY
|
|
12
|
+
* and LAYA_BASE_URL) itself.
|
|
12
13
|
*/
|
|
13
14
|
import type { CommandModule } from "yargs";
|
|
14
15
|
import type { CliDecideArgs } from "../../types/index.js";
|
|
@@ -8,11 +8,13 @@
|
|
|
8
8
|
* for the full vocabulary.
|
|
9
9
|
*
|
|
10
10
|
* Credentials are env-only here, exactly as every other CLI command: the
|
|
11
|
-
* decision provider
|
|
11
|
+
* decision provider reads its own settings (TYPESAFE_API_KEY, or LAYA_API_KEY
|
|
12
|
+
* and LAYA_BASE_URL) itself.
|
|
12
13
|
*/
|
|
13
14
|
import chalk from "chalk";
|
|
14
15
|
import ora from "ora";
|
|
15
16
|
import fs from "node:fs";
|
|
17
|
+
import { describeDecisionProviderKeys } from "../../factories/providerDescriptors.js";
|
|
16
18
|
import { NeuroLink } from "../../neurolink.js";
|
|
17
19
|
import { readDecisionChoice } from "../../utils/decisionAnswers.js";
|
|
18
20
|
import { calculateCost, hasPricing } from "../../utils/pricing.js";
|
|
@@ -87,7 +89,7 @@ function describeDecideError(error) {
|
|
|
87
89
|
}
|
|
88
90
|
const message = error instanceof Error ? error.message : String(error);
|
|
89
91
|
if (message.includes("No decision provider is configured")) {
|
|
90
|
-
return
|
|
92
|
+
return `No decision provider is configured. Set ${describeDecisionProviderKeys()}.`;
|
|
91
93
|
}
|
|
92
94
|
return message;
|
|
93
95
|
}
|
|
@@ -20,7 +20,7 @@ import { handleGCPSetup } from "./setup-gcp.js";
|
|
|
20
20
|
import { handleHuggingFaceSetup } from "./setup-huggingface.js";
|
|
21
21
|
import { handleMistralSetup } from "./setup-mistral.js";
|
|
22
22
|
import { PROVIDER_DESCRIPTORS_BY_NAME } from "../../factories/providerDescriptors.js";
|
|
23
|
-
import { createCohereConfig, createDeepSeekConfig, createIdeogramConfig, createJinaConfig, createNvidiaNimConfig, createOpenAICompatibleConfig, createRecraftConfig, createReplicateConfig, createStabilityConfig, createVoyageConfig, createTypeSafeConfig, satisfiesFallbacks, } from "../../utils/providerConfig.js";
|
|
23
|
+
import { createCohereConfig, createDeepSeekConfig, createIdeogramConfig, createJinaConfig, createNvidiaNimConfig, createOpenAICompatibleConfig, createRecraftConfig, createReplicateConfig, createStabilityConfig, createVoyageConfig, createTypeSafeConfig, createLayaConfig, satisfiesFallbacks, } from "../../utils/providerConfig.js";
|
|
24
24
|
import { getCatalogJsonEntries, buildCatalogConfigOptions, } from "../../providers/catalog/loader.js";
|
|
25
25
|
// Provider information database
|
|
26
26
|
const PROVIDERS = [
|
|
@@ -169,6 +169,7 @@ export const EXTRA_PROVIDER_CONFIGS = {
|
|
|
169
169
|
ideogram: createIdeogramConfig(),
|
|
170
170
|
recraft: createRecraftConfig(),
|
|
171
171
|
typesafe: createTypeSafeConfig(),
|
|
172
|
+
laya: createLayaConfig(),
|
|
172
173
|
...Object.fromEntries(getCatalogJsonEntries()
|
|
173
174
|
.filter((e) => e.id !== "mistral")
|
|
174
175
|
.map((e) => [e.id, buildCatalogConfigOptions(e)])),
|
|
@@ -593,7 +593,7 @@ export class CLICommandFactory {
|
|
|
593
593
|
},
|
|
594
594
|
classifierStrategy: {
|
|
595
595
|
type: "string",
|
|
596
|
-
description: "Classifier strategy: 'auto' (default — 'jev' when
|
|
596
|
+
description: "Classifier strategy: 'auto' (default — 'jev' when a decision provider is configured, such as TYPESAFE_API_KEY, or LAYA_API_KEY with LAYA_BASE_URL, else 'heuristic'), 'heuristic' (no LLM), 'llm' (a cheap model picks per prompt), or 'jev' (a System One decision model — TypeSafe Jev, or Laya — with calibrated confidence).",
|
|
597
597
|
choices: ["auto", "heuristic", "llm", "jev"],
|
|
598
598
|
alias: "classifier-strategy",
|
|
599
599
|
},
|
|
@@ -37,7 +37,7 @@ export function buildClassifierRouterConfigFromCli(flags) {
|
|
|
37
37
|
if (!flags.classifierRouter) {
|
|
38
38
|
return undefined;
|
|
39
39
|
}
|
|
40
|
-
// Defaults to "auto", which resolves to "jev" when
|
|
40
|
+
// Defaults to "auto", which resolves to "jev" when a decision provider's key is set
|
|
41
41
|
// and "heuristic" otherwise — so a configured key upgrades CLI routing with
|
|
42
42
|
// no flag change, and an unset one behaves exactly as before.
|
|
43
43
|
const strategy = isStrategyKind(flags.classifierStrategy)
|
|
@@ -44,6 +44,8 @@ export declare enum AIProviderName {
|
|
|
44
44
|
RECRAFT = "recraft",
|
|
45
45
|
/** TypeSafe (Jev) — serves the `decide` inference type only. */
|
|
46
46
|
TYPESAFE = "typesafe",
|
|
47
|
+
/** Laya (Convai, open weights) — serves the `decide` inference type only. */
|
|
48
|
+
LAYA = "laya",
|
|
47
49
|
AUTO = "auto"
|
|
48
50
|
}
|
|
49
51
|
/**
|
|
@@ -1047,3 +1049,16 @@ export declare enum TypeSafeModels {
|
|
|
1047
1049
|
JEV_LATEST = "jev-latest",
|
|
1048
1050
|
JEV_PREVIEW = "jev-preview"
|
|
1049
1051
|
}
|
|
1052
|
+
/**
|
|
1053
|
+
* Laya checkpoints. Hand-written: Laya is a Tier-3 provider, so it is not in
|
|
1054
|
+
* the provider catalog and codegen never touches this.
|
|
1055
|
+
*
|
|
1056
|
+
* `auto` sends no model, which lets Laya's own router pick a checkpoint by
|
|
1057
|
+
* the detected language of the state.
|
|
1058
|
+
*/
|
|
1059
|
+
export declare enum LayaModels {
|
|
1060
|
+
TYPED_DECISIONS = "typed-decisions",
|
|
1061
|
+
ENGLISH = "english",
|
|
1062
|
+
MULTILINGUAL = "multilingual",
|
|
1063
|
+
AUTO = "auto"
|
|
1064
|
+
}
|
package/dist/constants/enums.js
CHANGED
|
@@ -50,6 +50,8 @@ export var AIProviderName;
|
|
|
50
50
|
AIProviderName["RECRAFT"] = "recraft";
|
|
51
51
|
/** TypeSafe (Jev) — serves the `decide` inference type only. */
|
|
52
52
|
AIProviderName["TYPESAFE"] = "typesafe";
|
|
53
|
+
/** Laya (Convai, open weights) — serves the `decide` inference type only. */
|
|
54
|
+
AIProviderName["LAYA"] = "laya";
|
|
53
55
|
AIProviderName["AUTO"] = "auto";
|
|
54
56
|
})(AIProviderName || (AIProviderName = {}));
|
|
55
57
|
/**
|
|
@@ -1295,3 +1297,17 @@ export var TypeSafeModels;
|
|
|
1295
1297
|
TypeSafeModels["JEV_LATEST"] = "jev-latest";
|
|
1296
1298
|
TypeSafeModels["JEV_PREVIEW"] = "jev-preview";
|
|
1297
1299
|
})(TypeSafeModels || (TypeSafeModels = {}));
|
|
1300
|
+
/**
|
|
1301
|
+
* Laya checkpoints. Hand-written: Laya is a Tier-3 provider, so it is not in
|
|
1302
|
+
* the provider catalog and codegen never touches this.
|
|
1303
|
+
*
|
|
1304
|
+
* `auto` sends no model, which lets Laya's own router pick a checkpoint by
|
|
1305
|
+
* the detected language of the state.
|
|
1306
|
+
*/
|
|
1307
|
+
export var LayaModels;
|
|
1308
|
+
(function (LayaModels) {
|
|
1309
|
+
LayaModels["TYPED_DECISIONS"] = "typed-decisions";
|
|
1310
|
+
LayaModels["ENGLISH"] = "english";
|
|
1311
|
+
LayaModels["MULTILINGUAL"] = "multilingual";
|
|
1312
|
+
LayaModels["AUTO"] = "auto";
|
|
1313
|
+
})(LayaModels || (LayaModels = {}));
|
|
@@ -1,7 +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
|
+
import { DECISION_PROVIDERS, PROVIDER_DESCRIPTORS_BY_NAME, } from "../factories/providerDescriptors.js";
|
|
5
5
|
import { MiddlewareFactory } from "../middleware/factory.js";
|
|
6
6
|
import { modelSupports } from "../models/modelRegistry.js";
|
|
7
7
|
import { resolveRequestKind } from "./resolveRequestKind.js";
|
|
@@ -1854,7 +1854,7 @@ export class BaseProvider {
|
|
|
1854
1854
|
async decide(_request) {
|
|
1855
1855
|
logger.warn(`decide() called on ${this.providerName}, which does not serve the decide inference type`);
|
|
1856
1856
|
throw new Error(`The decide inference type is not supported by the ${this.providerName} provider. ` +
|
|
1857
|
-
`Supported providers:
|
|
1857
|
+
`Supported providers: ${DECISION_PROVIDERS.map((d) => d.name).join(", ")}. A provider declares this capability with ` +
|
|
1858
1858
|
`"decide" in its descriptor's inferenceKinds.`);
|
|
1859
1859
|
}
|
|
1860
1860
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AIProviderName } from "../constants/enums.js";
|
|
2
|
-
import type { ProviderDescriptor } from "../types/index.js";
|
|
2
|
+
import type { NeurolinkCredentials, ProviderDescriptor } from "../types/index.js";
|
|
3
3
|
/**
|
|
4
4
|
* Single source of truth for provider identity, credentials, defaults, and
|
|
5
5
|
* runtime behavior classification — the hand-maintained providers plus the
|
|
@@ -21,11 +21,20 @@ export declare const PROVIDER_ALIAS_INDEX: ReadonlyMap<string, AIProviderName>;
|
|
|
21
21
|
*/
|
|
22
22
|
export declare const DECISION_PROVIDERS: readonly ProviderDescriptor[];
|
|
23
23
|
/**
|
|
24
|
-
* The decision provider to use when a caller names none: the first
|
|
25
|
-
*
|
|
24
|
+
* The decision provider to use when a caller names none: the first
|
|
25
|
+
* DECISION_PROVIDERS entry that is fully configured, from the environment or
|
|
26
|
+
* from `credentials`.
|
|
26
27
|
*
|
|
27
|
-
* This is where "if somebody
|
|
28
|
-
* Returns undefined when none is configured, which every
|
|
29
|
-
* treats as "carry on exactly as before".
|
|
28
|
+
* This is where "if somebody configures it, we start using it" is
|
|
29
|
+
* implemented. Returns undefined when none is configured, which every
|
|
30
|
+
* internal consumer treats as "carry on exactly as before".
|
|
30
31
|
*/
|
|
31
|
-
export declare function resolveDefaultDecisionProvider(): string | undefined;
|
|
32
|
+
export declare function resolveDefaultDecisionProvider(credentials?: NeurolinkCredentials): string | undefined;
|
|
33
|
+
/**
|
|
34
|
+
* The variables that would configure each decision provider, one clause per
|
|
35
|
+
* provider in precedence order, e.g. "TYPESAFE_API_KEY or AI_GATEWAY_API_KEY
|
|
36
|
+
* for typesafe, or LAYA_API_KEY and LAYA_BASE_URL for laya". Derived from
|
|
37
|
+
* DECISION_PROVIDERS so a new decision provider appears in every "nothing is
|
|
38
|
+
* configured" message without anyone editing one.
|
|
39
|
+
*/
|
|
40
|
+
export declare function describeDecisionProviderKeys(): string;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AIProviderName } from "../constants/enums.js";
|
|
2
|
-
import { GoogleAIModels, OpenAIModels, AnthropicModels, VertexModels, OllamaModels, LiteLLMModels, NvidiaNimModels, OpenRouterModels, CohereModels, VoyageModels, TypeSafeModels, JinaModels, StabilityModels, IdeogramModels, RecraftModels, ReplicateModels, } from "../constants/enums.js";
|
|
2
|
+
import { GoogleAIModels, OpenAIModels, AnthropicModels, VertexModels, OllamaModels, LiteLLMModels, NvidiaNimModels, OpenRouterModels, CohereModels, VoyageModels, TypeSafeModels, LayaModels, JinaModels, StabilityModels, IdeogramModels, RecraftModels, ReplicateModels, } from "../constants/enums.js";
|
|
3
3
|
import { API_KEY_FORMATS } from "../utils/providerConfig.js";
|
|
4
4
|
import { getCatalogJsonEntries, catalogCredentialsKey, catalogEnvVar, } from "../providers/catalog/loader.js";
|
|
5
5
|
import { DEFAULT_INFERENCE_KINDS } from "../types/index.js";
|
|
@@ -451,6 +451,56 @@ const HAND_DESCRIPTORS = [
|
|
|
451
451
|
timeouts: { decideMs: 5000 },
|
|
452
452
|
setupUrl: "https://console.typesafe.ai/keys",
|
|
453
453
|
},
|
|
454
|
+
// Laya MUST stay after TypeSafe. resolveDefaultDecisionProvider() returns
|
|
455
|
+
// the first configured DECISION_PROVIDERS entry, in this order, so a host
|
|
456
|
+
// configured for both keeps Jev for every built-in consumer and reaches
|
|
457
|
+
// Laya only by naming it. Reordering these two silently changes which model
|
|
458
|
+
// routes, compacts and plans for every such host.
|
|
459
|
+
{
|
|
460
|
+
name: AIProviderName.LAYA,
|
|
461
|
+
aliases: [],
|
|
462
|
+
credentialsKey: "laya",
|
|
463
|
+
envVars: {
|
|
464
|
+
apiKey: "LAYA_API_KEY",
|
|
465
|
+
baseURL: "LAYA_BASE_URL",
|
|
466
|
+
// Laya has no built-in endpoint (it is self-hosted or behind a proxy),
|
|
467
|
+
// so a key is useless without a base URL. Both are required for it to
|
|
468
|
+
// count as configured, from the environment or credentials.laya.
|
|
469
|
+
extraRequired: ["LAYA_BASE_URL"],
|
|
470
|
+
model: "LAYA_MODEL",
|
|
471
|
+
},
|
|
472
|
+
defaultModel: LayaModels.TYPED_DECISIONS,
|
|
473
|
+
// Serves only `decide`, like TypeSafe — the one declaration that keeps it
|
|
474
|
+
// out of every generation code path.
|
|
475
|
+
inferenceKinds: ["decide"],
|
|
476
|
+
toolSupport: "none",
|
|
477
|
+
localRuntime: false,
|
|
478
|
+
healthCheck: "env-only",
|
|
479
|
+
// Deliberately NO autoSelectPriority / autoSelectPreference /
|
|
480
|
+
// defaultHealthSweepPriority, for the same reason as TypeSafe above.
|
|
481
|
+
timeouts: { decideMs: 5000 },
|
|
482
|
+
// Laya's encoders cut the state off silently past their window (measured
|
|
483
|
+
// live: a 60,000-character state answered 200 on 1,024 tokens). 768 is
|
|
484
|
+
// the 1,024-token window minus the 256-token budget Laya reserves for a
|
|
485
|
+
// question and its options; 320 is the English checkpoint's 512 minus
|
|
486
|
+
// 192. `auto` may route to English, and an unlisted name (Laya's server
|
|
487
|
+
// accepts aliases such as `en`) could be any checkpoint, so both get 320.
|
|
488
|
+
// 64 is the question cap in Laya's own server. Non-ASCII rates were
|
|
489
|
+
// measured live: Chinese is ~1.4 tokens per character on the English
|
|
490
|
+
// tokenizer (typed-decisions, english) and ~0.56 on multilingual.
|
|
491
|
+
decisionLimits: {
|
|
492
|
+
maxStateTokens: 320,
|
|
493
|
+
maxQuestions: 64,
|
|
494
|
+
nonAsciiTokensPerChar: 1.5,
|
|
495
|
+
models: {
|
|
496
|
+
"typed-decisions": { maxStateTokens: 768 },
|
|
497
|
+
multilingual: { maxStateTokens: 768, nonAsciiTokensPerChar: 0.6 },
|
|
498
|
+
english: { maxStateTokens: 320 },
|
|
499
|
+
auto: { maxStateTokens: 320 },
|
|
500
|
+
},
|
|
501
|
+
},
|
|
502
|
+
setupUrl: "https://github.com/NandhaKishorM/laya",
|
|
503
|
+
},
|
|
454
504
|
];
|
|
455
505
|
/**
|
|
456
506
|
* Builds a ProviderDescriptor for every JSON-catalog provider. Every field
|
|
@@ -554,20 +604,52 @@ export const PROVIDER_ALIAS_INDEX = new Map(PROVIDER_DESCRIPTORS.flatMap((d) =>
|
|
|
554
604
|
*/
|
|
555
605
|
export const DECISION_PROVIDERS = PROVIDER_DESCRIPTORS.filter((d) => (d.inferenceKinds ?? DEFAULT_INFERENCE_KINDS).includes("decide"));
|
|
556
606
|
/**
|
|
557
|
-
*
|
|
558
|
-
*
|
|
607
|
+
* Whether a decision provider is fully configured: its key and every
|
|
608
|
+
* extraRequired variable are set, each either in the environment or in
|
|
609
|
+
* `credentials` — the same config a caller passes to
|
|
610
|
+
* `new NeuroLink({ credentials })` or per call. Whitespace counts as unset.
|
|
611
|
+
*/
|
|
612
|
+
function isDecisionProviderConfigured(descriptor, credentials) {
|
|
613
|
+
const isSet = (value) => typeof value === "string" && value.trim() !== "";
|
|
614
|
+
const inEnv = (name) => name !== undefined && isSet(process.env[name]);
|
|
615
|
+
const slice = descriptor.credentialsKey
|
|
616
|
+
? credentials?.[descriptor.credentialsKey]
|
|
617
|
+
: undefined;
|
|
618
|
+
// `gatewayApiKey` is TypeSafe's config form of AI_GATEWAY_API_KEY.
|
|
619
|
+
const hasKey = [descriptor.envVars.apiKey, ...(descriptor.envVars.fallbacks ?? [])].some(inEnv) ||
|
|
620
|
+
isSet(slice?.apiKey) ||
|
|
621
|
+
isSet(slice?.gatewayApiKey);
|
|
622
|
+
// A required base URL can also come from credentials.<key>.baseURL.
|
|
623
|
+
const hasRequired = (descriptor.envVars.extraRequired ?? []).every((name) => inEnv(name) ||
|
|
624
|
+
(name === descriptor.envVars.baseURL && isSet(slice?.baseURL)));
|
|
625
|
+
return hasKey && hasRequired;
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* The decision provider to use when a caller names none: the first
|
|
629
|
+
* DECISION_PROVIDERS entry that is fully configured, from the environment or
|
|
630
|
+
* from `credentials`.
|
|
559
631
|
*
|
|
560
|
-
* This is where "if somebody
|
|
561
|
-
* Returns undefined when none is configured, which every
|
|
562
|
-
* treats as "carry on exactly as before".
|
|
632
|
+
* This is where "if somebody configures it, we start using it" is
|
|
633
|
+
* implemented. Returns undefined when none is configured, which every
|
|
634
|
+
* internal consumer treats as "carry on exactly as before".
|
|
563
635
|
*/
|
|
564
|
-
export function resolveDefaultDecisionProvider() {
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
636
|
+
export function resolveDefaultDecisionProvider(credentials) {
|
|
637
|
+
return DECISION_PROVIDERS.find((descriptor) => isDecisionProviderConfigured(descriptor, credentials))?.name;
|
|
638
|
+
}
|
|
639
|
+
/**
|
|
640
|
+
* The variables that would configure each decision provider, one clause per
|
|
641
|
+
* provider in precedence order, e.g. "TYPESAFE_API_KEY or AI_GATEWAY_API_KEY
|
|
642
|
+
* for typesafe, or LAYA_API_KEY and LAYA_BASE_URL for laya". Derived from
|
|
643
|
+
* DECISION_PROVIDERS so a new decision provider appears in every "nothing is
|
|
644
|
+
* configured" message without anyone editing one.
|
|
645
|
+
*/
|
|
646
|
+
export function describeDecisionProviderKeys() {
|
|
647
|
+
return DECISION_PROVIDERS.map((descriptor) => {
|
|
648
|
+
const keys = [
|
|
649
|
+
descriptor.envVars.apiKey,
|
|
650
|
+
...(descriptor.envVars.fallbacks ?? []),
|
|
651
|
+
].filter((name) => typeof name === "string" && name !== "");
|
|
652
|
+
const required = descriptor.envVars.extraRequired ?? [];
|
|
653
|
+
return `${[keys.join(" or "), ...required].join(" and ")} for ${descriptor.name}`;
|
|
654
|
+
}).join(", or ");
|
|
573
655
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ProviderFactory } from "./providerFactory.js";
|
|
2
2
|
import { logger } from "../utils/logger.js";
|
|
3
|
-
import { AIProviderName, GoogleAIModels, OpenAIModels, AnthropicModels, VertexModels, OllamaModels, LiteLLMModels, NvidiaNimModels, OpenRouterModels, CohereModels, VoyageModels, JinaModels, StabilityModels, IdeogramModels, RecraftModels, TypeSafeModels, ReplicateModels, } from "../constants/enums.js";
|
|
3
|
+
import { AIProviderName, GoogleAIModels, OpenAIModels, AnthropicModels, VertexModels, OllamaModels, LiteLLMModels, NvidiaNimModels, OpenRouterModels, CohereModels, VoyageModels, JinaModels, StabilityModels, IdeogramModels, RecraftModels, TypeSafeModels, LayaModels, 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
6
|
import { providerChoicesFor } from "./mediaHandlerCatalog.js";
|
|
@@ -234,6 +234,14 @@ export class ProviderRegistry {
|
|
|
234
234
|
const { TypeSafeProvider } = await import("../providers/typesafe.js");
|
|
235
235
|
return new TypeSafeProvider(modelName, sdk, undefined, typesafeCreds);
|
|
236
236
|
}, process.env.TYPESAFE_MODEL || TypeSafeModels.JEV_LATEST, ["jev", "typesafe-ai"], PROVIDER_DESCRIPTORS_BY_NAME.get(AIProviderName.TYPESAFE));
|
|
237
|
+
// Register Laya — the second `decide` provider, reached at whatever base
|
|
238
|
+
// URL its config names. Its descriptor declares inferenceKinds:
|
|
239
|
+
// ["decide"], so nothing in the generation fallback chain can reach it.
|
|
240
|
+
ProviderFactory.registerProvider(AIProviderName.LAYA, async (modelName, _providerName, sdk, _region, credentials) => {
|
|
241
|
+
const layaCreds = credentials;
|
|
242
|
+
const { LayaProvider } = await import("../providers/laya.js");
|
|
243
|
+
return new LayaProvider(modelName, sdk, undefined, layaCreds);
|
|
244
|
+
}, process.env.LAYA_MODEL || LayaModels.TYPED_DECISIONS, [], PROVIDER_DESCRIPTORS_BY_NAME.get(AIProviderName.LAYA));
|
|
237
245
|
logger.debug("All AI providers registered successfully");
|
|
238
246
|
// ===== MEDIA HANDLER REGISTRATION =====
|
|
239
247
|
// Single registration path (Task 11): each ecosystem barrel (voice,
|
|
@@ -1702,11 +1702,25 @@ export class ExternalServerManager extends EventEmitter {
|
|
|
1702
1702
|
instance.metrics.averageResponseTime =
|
|
1703
1703
|
totalTime / instance.metrics.totalToolCalls;
|
|
1704
1704
|
if (result.success) {
|
|
1705
|
-
|
|
1705
|
+
// A resolved `{ isError: true }` still comes back with success:true so
|
|
1706
|
+
// the caller receives the MCP error payload as-is, but it is a failed
|
|
1707
|
+
// call for telemetry: label mcp_tool_calls_total accordingly instead
|
|
1708
|
+
// of counting every resolved error as a success.
|
|
1709
|
+
const resolvedError = result.isErrorResult === true;
|
|
1710
|
+
if (resolvedError) {
|
|
1711
|
+
// Keep this instance's totalErrors (and the getStatistics() /
|
|
1712
|
+
// getExternalMCPStatistics() it feeds) consistent with the breaker,
|
|
1713
|
+
// TelemetryService and ToolDiscoveryService's failedCalls, all of
|
|
1714
|
+
// which already count a resolved isError result as a failure.
|
|
1715
|
+
instance.metrics.totalErrors++;
|
|
1716
|
+
}
|
|
1717
|
+
mcpLogger.debug(resolvedError
|
|
1718
|
+
? `[ExternalServerManager] Tool returned a resolved MCP error: ${toolName} on ${serverId}`
|
|
1719
|
+
: `[ExternalServerManager] Tool executed successfully: ${toolName} on ${serverId}`, {
|
|
1706
1720
|
duration,
|
|
1707
1721
|
});
|
|
1708
1722
|
try {
|
|
1709
|
-
TelemetryService.getInstance()?.recordMCPToolCall(toolName, duration,
|
|
1723
|
+
TelemetryService.getInstance()?.recordMCPToolCall(toolName, duration, !resolvedError);
|
|
1710
1724
|
}
|
|
1711
1725
|
catch {
|
|
1712
1726
|
/* telemetry should not break execution */
|
|
@@ -21,9 +21,30 @@ export declare class MCPCircuitBreaker extends EventEmitter {
|
|
|
21
21
|
private cleanupTimer?;
|
|
22
22
|
constructor(name: string, config?: Partial<CircuitBreakerConfig>);
|
|
23
23
|
/**
|
|
24
|
-
* Execute an operation with circuit breaker protection
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
* Execute an operation with circuit breaker protection.
|
|
25
|
+
*
|
|
26
|
+
* `operation` is handed a `recordResolvedFailure` callback so it can flag a
|
|
27
|
+
* *resolved* result as a logical failure without throwing. This matters for
|
|
28
|
+
* MCP tool calls: the client does not throw on a protocol error, it
|
|
29
|
+
* resolves with `{ isError: true, ... }`. Before this callback existed,
|
|
30
|
+
* such a call always fell through to the success path below — the breaker
|
|
31
|
+
* could never open for a tool that only ever "fails" by resolving an error
|
|
32
|
+
* result, and callers still need that resolved value returned as-is (not
|
|
33
|
+
* replaced by a thrown transport error). Call `recordResolvedFailure()`
|
|
34
|
+
* from inside `operation` when that resolved value represents a failure;
|
|
35
|
+
* the call is still counted as a normal, non-throwing success from the
|
|
36
|
+
* `Promise.race` below, so this callback is what tells `execute()`
|
|
37
|
+
* otherwise. Callers that never call it see no behavior change.
|
|
38
|
+
*/
|
|
39
|
+
execute<T>(operation: (recordResolvedFailure: (reason?: string) => void) => Promise<T>): Promise<T>;
|
|
40
|
+
/**
|
|
41
|
+
* Shared bookkeeping for a failed outcome — a thrown error, or a resolved
|
|
42
|
+
* result the caller flagged via `recordResolvedFailure`. Records the call
|
|
43
|
+
* as a failure, emits `callFailure`, and runs the state transitions that
|
|
44
|
+
* used to live inline in the `catch` block: half-open failure immediately
|
|
45
|
+
* re-opens the circuit; closed checks the failure threshold.
|
|
46
|
+
*/
|
|
47
|
+
private recordFailureOutcome;
|
|
27
48
|
/**
|
|
28
49
|
* Record a call in the history
|
|
29
50
|
*/
|
|
@@ -46,7 +46,20 @@ export class MCPCircuitBreaker extends EventEmitter {
|
|
|
46
46
|
this.cleanupTimer = setInterval(() => this.cleanupCallHistory(), 60000);
|
|
47
47
|
}
|
|
48
48
|
/**
|
|
49
|
-
* Execute an operation with circuit breaker protection
|
|
49
|
+
* Execute an operation with circuit breaker protection.
|
|
50
|
+
*
|
|
51
|
+
* `operation` is handed a `recordResolvedFailure` callback so it can flag a
|
|
52
|
+
* *resolved* result as a logical failure without throwing. This matters for
|
|
53
|
+
* MCP tool calls: the client does not throw on a protocol error, it
|
|
54
|
+
* resolves with `{ isError: true, ... }`. Before this callback existed,
|
|
55
|
+
* such a call always fell through to the success path below — the breaker
|
|
56
|
+
* could never open for a tool that only ever "fails" by resolving an error
|
|
57
|
+
* result, and callers still need that resolved value returned as-is (not
|
|
58
|
+
* replaced by a thrown transport error). Call `recordResolvedFailure()`
|
|
59
|
+
* from inside `operation` when that resolved value represents a failure;
|
|
60
|
+
* the call is still counted as a normal, non-throwing success from the
|
|
61
|
+
* `Promise.race` below, so this callback is what tells `execute()`
|
|
62
|
+
* otherwise. Callers that never call it see no behavior change.
|
|
50
63
|
*/
|
|
51
64
|
async execute(operation) {
|
|
52
65
|
const startTime = Date.now();
|
|
@@ -91,11 +104,29 @@ export class MCPCircuitBreaker extends EventEmitter {
|
|
|
91
104
|
});
|
|
92
105
|
}
|
|
93
106
|
}
|
|
107
|
+
// resolvedFailureReason is set (via recordResolvedFailure, passed into
|
|
108
|
+
// operation() below) when operation() resolves normally but represents
|
|
109
|
+
// a logical failure — e.g. an MCP tool call that resolved
|
|
110
|
+
// { isError: true } instead of throwing. It is call-scoped (a local,
|
|
111
|
+
// not instance state), so concurrent execute() calls on the same
|
|
112
|
+
// breaker (the same tool invoked in parallel) can't cross-contaminate
|
|
113
|
+
// each other's outcome.
|
|
114
|
+
let resolvedFailureReason;
|
|
115
|
+
const recordResolvedFailure = (reason) => {
|
|
116
|
+
resolvedFailureReason = reason || "Operation resolved a failure result";
|
|
117
|
+
};
|
|
94
118
|
// Execute operation with timeout
|
|
95
119
|
const result = await Promise.race([
|
|
96
|
-
operation(),
|
|
120
|
+
operation(recordResolvedFailure),
|
|
97
121
|
this.timeoutPromise(this.config.operationTimeout),
|
|
98
122
|
]);
|
|
123
|
+
if (resolvedFailureReason !== undefined) {
|
|
124
|
+
// Same bookkeeping as a thrown error below, but the resolved value
|
|
125
|
+
// is still returned to the caller unchanged — no transport error is
|
|
126
|
+
// synthesized for a result the protocol itself resolved successfully.
|
|
127
|
+
this.recordFailureOutcome(resolvedFailureReason, Date.now() - startTime);
|
|
128
|
+
return result;
|
|
129
|
+
}
|
|
99
130
|
// Record successful call
|
|
100
131
|
this.recordCall(true, Date.now() - startTime);
|
|
101
132
|
// Handle half-open success
|
|
@@ -111,26 +142,34 @@ export class MCPCircuitBreaker extends EventEmitter {
|
|
|
111
142
|
catch (error) {
|
|
112
143
|
// Record failed call
|
|
113
144
|
const duration = Date.now() - startTime;
|
|
114
|
-
this.recordCall(false, duration);
|
|
115
145
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
116
|
-
|
|
117
|
-
this.emit("callFailure", {
|
|
118
|
-
error: errorMessage,
|
|
119
|
-
duration,
|
|
120
|
-
timestamp: new Date(),
|
|
121
|
-
});
|
|
122
|
-
// Handle state transitions on failure
|
|
123
|
-
if (this.state === "half-open") {
|
|
124
|
-
// Failure in half-open immediately opens circuit
|
|
125
|
-
this.changeState("open", `Half-open test failed: ${errorMessage}`);
|
|
126
|
-
}
|
|
127
|
-
else if (this.state === "closed") {
|
|
128
|
-
// Check if we should open the circuit
|
|
129
|
-
this.checkFailureThreshold();
|
|
130
|
-
}
|
|
146
|
+
this.recordFailureOutcome(errorMessage, duration);
|
|
131
147
|
throw error;
|
|
132
148
|
}
|
|
133
149
|
}
|
|
150
|
+
/**
|
|
151
|
+
* Shared bookkeeping for a failed outcome — a thrown error, or a resolved
|
|
152
|
+
* result the caller flagged via `recordResolvedFailure`. Records the call
|
|
153
|
+
* as a failure, emits `callFailure`, and runs the state transitions that
|
|
154
|
+
* used to live inline in the `catch` block: half-open failure immediately
|
|
155
|
+
* re-opens the circuit; closed checks the failure threshold.
|
|
156
|
+
*/
|
|
157
|
+
recordFailureOutcome(reason, duration) {
|
|
158
|
+
this.recordCall(false, duration);
|
|
159
|
+
this.emit("callFailure", {
|
|
160
|
+
error: reason,
|
|
161
|
+
duration,
|
|
162
|
+
timestamp: new Date(),
|
|
163
|
+
});
|
|
164
|
+
if (this.state === "half-open") {
|
|
165
|
+
// Failure in half-open immediately opens circuit
|
|
166
|
+
this.changeState("open", `Half-open test failed: ${reason}`);
|
|
167
|
+
}
|
|
168
|
+
else if (this.state === "closed") {
|
|
169
|
+
// Check if we should open the circuit
|
|
170
|
+
this.checkFailureThreshold();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
134
173
|
/**
|
|
135
174
|
* Record a call in the history
|
|
136
175
|
*/
|
|
@@ -435,8 +435,13 @@ export class ToolDiscoveryService extends EventEmitter {
|
|
|
435
435
|
resetTimeout: 30000,
|
|
436
436
|
operationTimeout: effectiveTimeout,
|
|
437
437
|
});
|
|
438
|
-
// Execute tool with circuit breaker protection
|
|
439
|
-
|
|
438
|
+
// Execute tool with circuit breaker protection. `recordResolvedFailure`
|
|
439
|
+
// lets the callback below tell the breaker that a *resolved* MCP
|
|
440
|
+
// isError result is a failure — the MCP client does not throw on a
|
|
441
|
+
// protocol error, so without this the breaker would only ever see a
|
|
442
|
+
// clean resolve and could never open for a tool that fails this way.
|
|
443
|
+
let isErrorResultDetected = false;
|
|
444
|
+
const result = await circuitBreaker.execute(async (recordResolvedFailure) => {
|
|
440
445
|
return mcpTracer.startActiveSpan("neurolink.mcp.callTool", {
|
|
441
446
|
kind: SpanKind.CLIENT,
|
|
442
447
|
attributes: {
|
|
@@ -479,6 +484,11 @@ export class ToolDiscoveryService extends EventEmitter {
|
|
|
479
484
|
code: SpanStatusCode.ERROR,
|
|
480
485
|
message: errorText || `Tool ${toolName} returned isError`,
|
|
481
486
|
});
|
|
487
|
+
// NLK: count a resolved isError result as a breaker failure
|
|
488
|
+
// and label completion telemetry as error below — without
|
|
489
|
+
// throwing, so the protocol result is still returned as-is.
|
|
490
|
+
isErrorResultDetected = true;
|
|
491
|
+
recordResolvedFailure(errorText || `Tool ${toolName} returned isError`);
|
|
482
492
|
}
|
|
483
493
|
else {
|
|
484
494
|
callSpan.setStatus({ code: SpanStatusCode.OK });
|
|
@@ -528,8 +538,13 @@ export class ToolDiscoveryService extends EventEmitter {
|
|
|
528
538
|
});
|
|
529
539
|
});
|
|
530
540
|
const duration = Date.now() - startTime;
|
|
531
|
-
// Update tool statistics
|
|
532
|
-
|
|
541
|
+
// Update tool statistics. A resolved isError result is a failed call
|
|
542
|
+
// for completion telemetry purposes even though the wrapper below
|
|
543
|
+
// still returns success:true / data:result unchanged — flipping
|
|
544
|
+
// `success` here would make externalServerManager.executeTool() throw
|
|
545
|
+
// instead of returning the resolved MCP error, which is exactly the
|
|
546
|
+
// "thrown transport error" this fix must not introduce.
|
|
547
|
+
this.updateToolStats(toolKey, !isErrorResultDetected, duration);
|
|
533
548
|
// Validate output if requested
|
|
534
549
|
if (options.validateOutput !== false) {
|
|
535
550
|
this.validateToolOutput(result);
|
|
@@ -541,6 +556,7 @@ export class ToolDiscoveryService extends EventEmitter {
|
|
|
541
556
|
return {
|
|
542
557
|
success: true,
|
|
543
558
|
data: result,
|
|
559
|
+
isErrorResult: isErrorResultDetected,
|
|
544
560
|
duration,
|
|
545
561
|
metadata: {
|
|
546
562
|
toolName,
|
|
@@ -20,6 +20,7 @@ import { llamacppManifest } from "./manifests/llamacpp.js";
|
|
|
20
20
|
import { xaiManifest } from "./manifests/xai.js";
|
|
21
21
|
import { groqManifest } from "./manifests/groq.js";
|
|
22
22
|
import { typesafeManifest } from "./manifests/typesafe.js";
|
|
23
|
+
import { layaManifest } from "./manifests/laya.js";
|
|
23
24
|
import { cohereManifest } from "./manifests/cohere.js";
|
|
24
25
|
import { togetherAiManifest } from "./manifests/together-ai.js";
|
|
25
26
|
import { fireworksManifest } from "./manifests/fireworks.js";
|
|
@@ -118,6 +119,7 @@ export const MANIFEST_REGISTRY = {
|
|
|
118
119
|
xai: xaiManifest,
|
|
119
120
|
groq: groqManifest,
|
|
120
121
|
typesafe: typesafeManifest,
|
|
122
|
+
laya: layaManifest,
|
|
121
123
|
cerebras: catalogManifest("cerebras"),
|
|
122
124
|
sambanova: catalogManifest("sambanova"),
|
|
123
125
|
cohere: cohereManifest,
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ProviderModelManifest } from "../../types/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Laya — the `decide` inference type, not text generation.
|
|
4
|
+
*
|
|
5
|
+
* `contextWindow` is each checkpoint's encoder window, the whole budget for
|
|
6
|
+
* the state plus one question's instructions and options. `auto` carries the
|
|
7
|
+
* English figure because Laya's router may pick the English checkpoint.
|
|
8
|
+
*
|
|
9
|
+
* `maxOutputTokens` is required by the manifest type but has no honest value
|
|
10
|
+
* for a model that emits no text, so a small non-zero figure is used.
|
|
11
|
+
*/
|
|
12
|
+
export declare const layaManifest: ProviderModelManifest;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Laya — the `decide` inference type, not text generation.
|
|
3
|
+
*
|
|
4
|
+
* `contextWindow` is each checkpoint's encoder window, the whole budget for
|
|
5
|
+
* the state plus one question's instructions and options. `auto` carries the
|
|
6
|
+
* English figure because Laya's router may pick the English checkpoint.
|
|
7
|
+
*
|
|
8
|
+
* `maxOutputTokens` is required by the manifest type but has no honest value
|
|
9
|
+
* for a model that emits no text, so a small non-zero figure is used.
|
|
10
|
+
*/
|
|
11
|
+
export const layaManifest = {
|
|
12
|
+
defaultContextWindow: 1024,
|
|
13
|
+
models: {
|
|
14
|
+
_default: {
|
|
15
|
+
aliases: [],
|
|
16
|
+
contextWindow: 1024,
|
|
17
|
+
maxOutputTokens: 256,
|
|
18
|
+
vision: false,
|
|
19
|
+
functionCalling: false,
|
|
20
|
+
},
|
|
21
|
+
"typed-decisions": {
|
|
22
|
+
aliases: [],
|
|
23
|
+
displayName: "Laya (typed decisions)",
|
|
24
|
+
contextWindow: 1024,
|
|
25
|
+
maxOutputTokens: 256,
|
|
26
|
+
vision: false,
|
|
27
|
+
functionCalling: false,
|
|
28
|
+
},
|
|
29
|
+
multilingual: {
|
|
30
|
+
aliases: [],
|
|
31
|
+
displayName: "Laya (multilingual)",
|
|
32
|
+
contextWindow: 1024,
|
|
33
|
+
maxOutputTokens: 256,
|
|
34
|
+
vision: false,
|
|
35
|
+
functionCalling: false,
|
|
36
|
+
},
|
|
37
|
+
english: {
|
|
38
|
+
aliases: [],
|
|
39
|
+
displayName: "Laya (English)",
|
|
40
|
+
contextWindow: 512,
|
|
41
|
+
maxOutputTokens: 256,
|
|
42
|
+
vision: false,
|
|
43
|
+
functionCalling: false,
|
|
44
|
+
},
|
|
45
|
+
auto: {
|
|
46
|
+
aliases: [],
|
|
47
|
+
displayName: "Laya (routed by language)",
|
|
48
|
+
contextWindow: 512,
|
|
49
|
+
maxOutputTokens: 256,
|
|
50
|
+
vision: false,
|
|
51
|
+
functionCalling: false,
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
};
|