@juspay/neurolink 12.49.20 → 12.51.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.
Files changed (65) hide show
  1. package/CHANGELOG.md +3 -3
  2. package/dist/browser/neurolink.min.js +671 -671
  3. package/dist/cli/commands/setup.d.ts +7 -5
  4. package/dist/cli/commands/setup.js +10 -8
  5. package/dist/constants/enums.d.ts +7 -15
  6. package/dist/constants/enums.js +8 -16
  7. package/dist/core/conversationMemoryManager.js +10 -4
  8. package/dist/core/redisConversationMemoryManager.js +6 -5
  9. package/dist/factories/providerDescriptors.d.ts +17 -3
  10. package/dist/factories/providerDescriptors.js +81 -56
  11. package/dist/factories/providerRegistry.d.ts +9 -1
  12. package/dist/factories/providerRegistry.js +19 -9
  13. package/dist/models/manifestRegistry.js +40 -3
  14. package/dist/neurolink.d.ts +52 -1
  15. package/dist/neurolink.js +195 -0
  16. package/dist/providers/catalog/index.generated.d.ts +3 -1
  17. package/dist/providers/catalog/index.generated.js +11 -0
  18. package/dist/providers/catalog/laya.json +107 -0
  19. package/dist/providers/catalog/loader.d.ts +7 -4
  20. package/dist/providers/catalog/loader.js +10 -3
  21. package/dist/providers/catalog/provider-catalog.schema.json +218 -11
  22. package/dist/providers/catalog/schema.d.ts +11 -9
  23. package/dist/providers/catalog/schema.js +333 -59
  24. package/dist/providers/configuredDecision.d.ts +23 -0
  25. package/dist/providers/configuredDecision.js +111 -0
  26. package/dist/providers/decide/baseUrl.d.ts +2 -0
  27. package/dist/providers/decide/baseUrl.js +14 -0
  28. package/dist/providers/decide/errors.d.ts +8 -0
  29. package/dist/providers/decide/errors.js +57 -0
  30. package/dist/providers/decide/formatters.d.ts +2 -0
  31. package/dist/providers/decide/formatters.js +4 -0
  32. package/dist/providers/decide/paths.d.ts +1 -0
  33. package/dist/providers/decide/paths.js +21 -0
  34. package/dist/providers/decide/template.d.ts +3 -0
  35. package/dist/providers/decide/template.js +13 -0
  36. package/dist/types/conversation.d.ts +109 -0
  37. package/dist/types/livekit.d.ts +246 -3
  38. package/dist/types/providerCatalog.d.ts +74 -1
  39. package/dist/types/providerCatalog.generated.d.ts +2 -2
  40. package/dist/types/providers.d.ts +12 -9
  41. package/dist/types/sdk.d.ts +1 -1
  42. package/dist/types/stream.d.ts +8 -1
  43. package/dist/utils/conversationTurnIdentity.d.ts +24 -0
  44. package/dist/utils/conversationTurnIdentity.js +81 -0
  45. package/dist/utils/modelChoices.js +11 -25
  46. package/dist/utils/providerConfig.d.ts +0 -5
  47. package/dist/utils/providerConfig.js +0 -17
  48. package/dist/voice/livekit/brain.js +10 -7
  49. package/dist/voice/livekit/index.d.ts +2 -0
  50. package/dist/voice/livekit/index.js +2 -0
  51. package/dist/voice/livekit/memoryBinding.d.ts +47 -0
  52. package/dist/voice/livekit/memoryBinding.js +152 -0
  53. package/dist/voice/livekit/realtimeMcpTools.js +20 -1
  54. package/dist/voice/livekit/realtimeMemory.d.ts +22 -0
  55. package/dist/voice/livekit/realtimeMemory.js +111 -0
  56. package/dist/voice/livekit/realtimeVoiceAgent.js +40 -1
  57. package/dist/voice/livekit/turnRecorder.d.ts +38 -0
  58. package/dist/voice/livekit/turnRecorder.js +362 -0
  59. package/dist/voice/livekit/voiceAgent.js +43 -5
  60. package/docs-site/static/search-index.json +8 -7
  61. package/package.json +7 -2
  62. package/dist/models/manifests/laya.d.ts +0 -12
  63. package/dist/models/manifests/laya.js +0 -54
  64. package/dist/providers/laya.d.ts +0 -40
  65. package/dist/providers/laya.js +0 -152
@@ -7,16 +7,18 @@
7
7
  */
8
8
  import type { ProviderConfigOptions, SetupArgs } from "../../types/index.js";
9
9
  /**
10
- * ProviderConfigOptions for the 21 canonical AIProviderName values the
10
+ * ProviderConfigOptions for the canonical AIProviderName values the
11
11
  * interactive wizard above doesn't have a bespoke handleXSetup() for. 8 are
12
12
  * the JSON-catalog providers (cerebras, cloudflare, fireworks, groq,
13
13
  * perplexity, sambanova, together-ai, xai — mistral is the 9th catalog
14
14
  * provider but keeps its own handleMistralSetup() flow above, so it's
15
15
  * excluded here), spread directly from the catalog JSON via
16
- * buildCatalogConfigOptions(). 10 more reuse the existing createXConfig()
17
- * factories in providerConfig.ts; the remaining 5 (ollama, litellm,
18
- * sagemaker, lm-studio, llamacpp) don't have a factory and are defined
19
- * inline using their real env var names. lm-studio and llamacpp previously
16
+ * buildCatalogConfigOptions(); decide-kind catalog entries (laya) are spread
17
+ * the same way from getDecideCatalogEntries(). The hand rows listed first
18
+ * reuse the existing createXConfig() factories in providerConfig.ts; the
19
+ * remaining 5 (ollama, litellm, sagemaker, lm-studio, llamacpp) don't have a
20
+ * factory and are defined inline using their real env var names. lm-studio
21
+ * and llamacpp previously
20
22
  * had createLmStudioConfig()/createLlamaCppConfig() factories, but those
21
23
  * were removed as dead code in a later cleanup — this mirrors that removal
22
24
  * rather than re-adding them.
@@ -21,8 +21,8 @@ import { handleHuggingFaceSetup } from "./setup-huggingface.js";
21
21
  import { handleMistralSetup } from "./setup-mistral.js";
22
22
  import { OpenRouterModels, } from "../../constants/enums.js";
23
23
  import { PROVIDER_DESCRIPTORS_BY_NAME } from "../../factories/providerDescriptors.js";
24
- import { createCohereConfig, createDeepSeekConfig, createIdeogramConfig, createJinaConfig, createNvidiaNimConfig, createOpenAICompatibleConfig, createRecraftConfig, createReplicateConfig, createStabilityConfig, createVoyageConfig, createTypeSafeConfig, createLayaConfig, createXorConfig, createPerplexityDeciderConfig, createCloudflareClefConfig, satisfiesFallbacks, } from "../../utils/providerConfig.js";
25
- import { getCatalogJsonEntries, buildCatalogConfigOptions, } from "../../providers/catalog/loader.js";
24
+ import { createCohereConfig, createDeepSeekConfig, createIdeogramConfig, createJinaConfig, createNvidiaNimConfig, createOpenAICompatibleConfig, createRecraftConfig, createReplicateConfig, createStabilityConfig, createVoyageConfig, createTypeSafeConfig, createXorConfig, createPerplexityDeciderConfig, createCloudflareClefConfig, satisfiesFallbacks, } from "../../utils/providerConfig.js";
25
+ import { getCatalogJsonEntries, getDecideCatalogEntries, buildCatalogConfigOptions, } from "../../providers/catalog/loader.js";
26
26
  // Provider information database
27
27
  const PROVIDERS = [
28
28
  {
@@ -144,16 +144,18 @@ const PROVIDERS = [
144
144
  },
145
145
  ];
146
146
  /**
147
- * ProviderConfigOptions for the 21 canonical AIProviderName values the
147
+ * ProviderConfigOptions for the canonical AIProviderName values the
148
148
  * interactive wizard above doesn't have a bespoke handleXSetup() for. 8 are
149
149
  * the JSON-catalog providers (cerebras, cloudflare, fireworks, groq,
150
150
  * perplexity, sambanova, together-ai, xai — mistral is the 9th catalog
151
151
  * provider but keeps its own handleMistralSetup() flow above, so it's
152
152
  * excluded here), spread directly from the catalog JSON via
153
- * buildCatalogConfigOptions(). 10 more reuse the existing createXConfig()
154
- * factories in providerConfig.ts; the remaining 5 (ollama, litellm,
155
- * sagemaker, lm-studio, llamacpp) don't have a factory and are defined
156
- * inline using their real env var names. lm-studio and llamacpp previously
153
+ * buildCatalogConfigOptions(); decide-kind catalog entries (laya) are spread
154
+ * the same way from getDecideCatalogEntries(). The hand rows listed first
155
+ * reuse the existing createXConfig() factories in providerConfig.ts; the
156
+ * remaining 5 (ollama, litellm, sagemaker, lm-studio, llamacpp) don't have a
157
+ * factory and are defined inline using their real env var names. lm-studio
158
+ * and llamacpp previously
157
159
  * had createLmStudioConfig()/createLlamaCppConfig() factories, but those
158
160
  * were removed as dead code in a later cleanup — this mirrors that removal
159
161
  * rather than re-adding them.
@@ -170,10 +172,10 @@ export const EXTRA_PROVIDER_CONFIGS = {
170
172
  ideogram: createIdeogramConfig(),
171
173
  recraft: createRecraftConfig(),
172
174
  typesafe: createTypeSafeConfig(),
173
- laya: createLayaConfig(),
174
175
  xor: createXorConfig(),
175
176
  "perplexity-decider": createPerplexityDeciderConfig(),
176
177
  "cloudflare-clef": createCloudflareClefConfig(),
178
+ ...Object.fromEntries(getDecideCatalogEntries().map((e) => [e.id, buildCatalogConfigOptions(e)])),
177
179
  ...Object.fromEntries(getCatalogJsonEntries()
178
180
  .filter((e) => e.id !== "mistral")
179
181
  .map((e) => [e.id, buildCatalogConfigOptions(e)])),
@@ -56,6 +56,7 @@ export declare enum AIProviderName {
56
56
  INFERENCE_NET = "inference-net",
57
57
  IO_INTELLIGENCE = "io-intelligence",
58
58
  KOSCOMPUTE = "koscompute",
59
+ LAYA = "laya",
59
60
  LEMONFOX_AI = "lemonfox-ai",
60
61
  LILAC = "lilac",
61
62
  LLMTECH = "llmtech",
@@ -109,8 +110,6 @@ export declare enum AIProviderName {
109
110
  RECRAFT = "recraft",
110
111
  /** TypeSafe (Jev) — serves the `decide` inference type only. */
111
112
  TYPESAFE = "typesafe",
112
- /** Laya (Convai, open weights) — serves the `decide` inference type only. */
113
- LAYA = "laya",
114
113
  /** XOR (Juspay, open weights) — serves the `decide` inference type only. */
115
114
  XOR = "xor",
116
115
  /**
@@ -1327,6 +1326,12 @@ export declare enum IoIntelligenceModels {
1327
1326
  export declare enum KoscomputeModels {
1328
1327
  QWEN_QWEN3_8_27B = "qwen/qwen3.8-27b"
1329
1328
  }
1329
+ export declare enum LayaModels {
1330
+ TYPED_DECISIONS = "typed-decisions",
1331
+ ENGLISH = "english",
1332
+ MULTILINGUAL = "multilingual",
1333
+ AUTO = "auto"
1334
+ }
1330
1335
  export declare enum LemonfoxAiModels {
1331
1336
  DEEPSEEK_V4_FLASH = "deepseek-v4-flash",
1332
1337
  MIMO_V2_5_PRO = "mimo-v2.5-pro"
@@ -1771,19 +1776,6 @@ export declare enum TypeSafeModels {
1771
1776
  JEV_LATEST = "jev-latest",
1772
1777
  JEV_PREVIEW = "jev-preview"
1773
1778
  }
1774
- /**
1775
- * Laya checkpoints. Hand-written: Laya is a Tier-3 provider, so it is not in
1776
- * the provider catalog and codegen never touches this.
1777
- *
1778
- * `auto` sends no model, which lets Laya's own router pick a checkpoint by
1779
- * the detected language of the state.
1780
- */
1781
- export declare enum LayaModels {
1782
- TYPED_DECISIONS = "typed-decisions",
1783
- ENGLISH = "english",
1784
- MULTILINGUAL = "multilingual",
1785
- AUTO = "auto"
1786
- }
1787
1779
  /**
1788
1780
  * XOR decision models. Hand-written: XOR is a Tier-3 provider, so it is not in
1789
1781
  * the provider catalog and codegen never touches this.
@@ -61,6 +61,7 @@ export var AIProviderName;
61
61
  AIProviderName["INFERENCE_NET"] = "inference-net";
62
62
  AIProviderName["IO_INTELLIGENCE"] = "io-intelligence";
63
63
  AIProviderName["KOSCOMPUTE"] = "koscompute";
64
+ AIProviderName["LAYA"] = "laya";
64
65
  AIProviderName["LEMONFOX_AI"] = "lemonfox-ai";
65
66
  AIProviderName["LILAC"] = "lilac";
66
67
  AIProviderName["LLMTECH"] = "llmtech";
@@ -115,8 +116,6 @@ export var AIProviderName;
115
116
  AIProviderName["RECRAFT"] = "recraft";
116
117
  /** TypeSafe (Jev) — serves the `decide` inference type only. */
117
118
  AIProviderName["TYPESAFE"] = "typesafe";
118
- /** Laya (Convai, open weights) — serves the `decide` inference type only. */
119
- AIProviderName["LAYA"] = "laya";
120
119
  /** XOR (Juspay, open weights) — serves the `decide` inference type only. */
121
120
  AIProviderName["XOR"] = "xor";
122
121
  /**
@@ -1603,6 +1602,13 @@ export var KoscomputeModels;
1603
1602
  (function (KoscomputeModels) {
1604
1603
  KoscomputeModels["QWEN_QWEN3_8_27B"] = "qwen/qwen3.8-27b";
1605
1604
  })(KoscomputeModels || (KoscomputeModels = {}));
1605
+ export var LayaModels;
1606
+ (function (LayaModels) {
1607
+ LayaModels["TYPED_DECISIONS"] = "typed-decisions";
1608
+ LayaModels["ENGLISH"] = "english";
1609
+ LayaModels["MULTILINGUAL"] = "multilingual";
1610
+ LayaModels["AUTO"] = "auto";
1611
+ })(LayaModels || (LayaModels = {}));
1606
1612
  export var LemonfoxAiModels;
1607
1613
  (function (LemonfoxAiModels) {
1608
1614
  LemonfoxAiModels["DEEPSEEK_V4_FLASH"] = "deepseek-v4-flash";
@@ -2093,20 +2099,6 @@ export var TypeSafeModels;
2093
2099
  TypeSafeModels["JEV_LATEST"] = "jev-latest";
2094
2100
  TypeSafeModels["JEV_PREVIEW"] = "jev-preview";
2095
2101
  })(TypeSafeModels || (TypeSafeModels = {}));
2096
- /**
2097
- * Laya checkpoints. Hand-written: Laya is a Tier-3 provider, so it is not in
2098
- * the provider catalog and codegen never touches this.
2099
- *
2100
- * `auto` sends no model, which lets Laya's own router pick a checkpoint by
2101
- * the detected language of the state.
2102
- */
2103
- export var LayaModels;
2104
- (function (LayaModels) {
2105
- LayaModels["TYPED_DECISIONS"] = "typed-decisions";
2106
- LayaModels["ENGLISH"] = "english";
2107
- LayaModels["MULTILINGUAL"] = "multilingual";
2108
- LayaModels["AUTO"] = "auto";
2109
- })(LayaModels || (LayaModels = {}));
2110
2102
  /**
2111
2103
  * XOR decision models. Hand-written: XOR is a Tier-3 provider, so it is not in
2112
2104
  * the provider catalog and codegen never touches this.
@@ -11,6 +11,7 @@ import { tracers, withSpan } from "../telemetry/index.js";
11
11
  import { ConversationMemoryError } from "../types/index.js";
12
12
  import { buildContextFromPointer, getEffectiveTokenThreshold, } from "../utils/conversationMemory.js";
13
13
  import { logger } from "../utils/logger.js";
14
+ import { assistantTurnIdentity, isTurnAlreadyStored, userTurnIdentity, } from "../utils/conversationTurnIdentity.js";
14
15
  import { extractIsErrorText } from "../utils/toolResultStatus.js";
15
16
  export class ConversationMemoryManager {
16
17
  sessions = new Map();
@@ -82,11 +83,14 @@ export class ConversationMemoryManager {
82
83
  session = this.createNewSession(options.sessionId, options.userId);
83
84
  this.sessions.set(options.sessionId, session);
84
85
  }
86
+ if (isTurnAlreadyStored(session.messages, options.turnId)) {
87
+ return;
88
+ }
85
89
  const tokenThreshold = options.providerDetails
86
90
  ? getEffectiveTokenThreshold(options.providerDetails.provider, options.providerDetails.model, this.config.tokenThreshold, session.tokenThreshold)
87
91
  : this.config.tokenThreshold || 50000;
88
- const userMsg = await this.validateAndPrepareMessage(options.userMessage, "user", tokenThreshold);
89
- const assistantMsg = await this.validateAndPrepareMessage(options.aiResponse, "assistant", tokenThreshold);
92
+ const userMsg = await this.validateAndPrepareMessage(options.userMessage, "user", tokenThreshold, userTurnIdentity(options));
93
+ const assistantMsg = await this.validateAndPrepareMessage(options.aiResponse, "assistant", tokenThreshold, assistantTurnIdentity(options));
90
94
  if (options.events && options.events.length > 0) {
91
95
  assistantMsg.events = options.events;
92
96
  }
@@ -147,8 +151,8 @@ export class ConversationMemoryManager {
147
151
  * Validate and prepare a message before adding to session
148
152
  * Truncates if message exceeds token limit
149
153
  */
150
- async validateAndPrepareMessage(content, role, threshold) {
151
- const id = randomUUID();
154
+ async validateAndPrepareMessage(content, role, threshold, identity) {
155
+ const id = identity?.id ?? randomUUID();
152
156
  const tokenCount = TokenUtils.estimateTokenCount(content);
153
157
  const maxMessageSize = Math.floor(threshold * MEMORY_THRESHOLD_PERCENTAGE);
154
158
  if (tokenCount > maxMessageSize) {
@@ -166,6 +170,7 @@ export class ConversationMemoryManager {
166
170
  content: truncated,
167
171
  timestamp: new Date().toISOString(),
168
172
  metadata: {
173
+ ...identity?.metadata,
169
174
  truncated: true,
170
175
  },
171
176
  };
@@ -175,6 +180,7 @@ export class ConversationMemoryManager {
175
180
  role,
176
181
  content,
177
182
  timestamp: new Date().toISOString(),
183
+ ...(identity?.metadata ? { metadata: identity.metadata } : {}),
178
184
  };
179
185
  }
180
186
  /**
@@ -15,6 +15,7 @@ import { withTimeout } from "../utils/errorHandling.js";
15
15
  import { buildContextFromPointer, getEffectiveTokenThreshold, } from "../utils/conversationMemory.js";
16
16
  import { runWithCurrentLangfuseContext } from "../services/server/ai/observability/instrumentation.js";
17
17
  import { logger } from "../utils/logger.js";
18
+ import { assistantTurnIdentity, isTurnAlreadyStored, turnMessageFields, userTurnIdentity, } from "../utils/conversationTurnIdentity.js";
18
19
  import { extractIsErrorText } from "../utils/toolResultStatus.js";
19
20
  import { deserializeConversation, encodeStoredMessages, getSessionMessagesKey, MESSAGES_KEY_SUFFIX, isSessionMessagesKey, parseStoredMessages, serializeConversationMetadata, usesSplitMessageStorage, getNormalizedConfig, getPooledRedisClient, getSessionKey, getUserSessionsKey, releasePooledRedisClient, scanKeys, serializeConversation, } from "../utils/redis.js";
20
21
  const redisTracer = tracers.redis;
@@ -393,6 +394,9 @@ export class RedisConversationMemoryManager {
393
394
  const redisKey = getSessionKey(this.redisConfig, options.sessionId, options.userId);
394
395
  const conversationData = await this.redisClient.get(redisKey);
395
396
  let conversation = await this.hydrateMessages(deserializeConversation(conversationData), redisKey);
397
+ if (isTurnAlreadyStored(conversation?.messages ?? [], options.turnId)) {
398
+ return;
399
+ }
396
400
  // Split-storage bookkeeping: whether this session already keeps its
397
401
  // messages in the companion LIST, and how many it held before this
398
402
  // turn — so only the new ones are appended rather than rewriting the
@@ -457,7 +461,7 @@ export class RedisConversationMemoryManager {
457
461
  ? getEffectiveTokenThreshold(options.providerDetails.provider, options.providerDetails.model, this.config.tokenThreshold, conversation.tokenThreshold)
458
462
  : this.config.tokenThreshold || 50000;
459
463
  const userMsg = {
460
- id: randomUUID(),
464
+ ...turnMessageFields(userTurnIdentity(options)),
461
465
  timestamp: options.startTimeStamp?.toISOString() || this.generateTimestamp(),
462
466
  role: "user",
463
467
  content: options.userMessage,
@@ -471,14 +475,11 @@ export class RedisConversationMemoryManager {
471
475
  conversation.messages.push(...options.skillMessages);
472
476
  }
473
477
  const assistantMsg = {
474
- id: randomUUID(),
478
+ ...turnMessageFields(assistantTurnIdentity(options, options.thoughtSignature)),
475
479
  timestamp: this.generateTimestamp(),
476
480
  role: "assistant",
477
481
  content: options.aiResponse,
478
482
  events: options.events || undefined,
479
- ...(options.thoughtSignature && {
480
- metadata: { thoughtSignature: options.thoughtSignature },
481
- }),
482
483
  };
483
484
  conversation.messages.push(assistantMsg);
484
485
  // Store API-reported token counts if available
@@ -1,5 +1,11 @@
1
1
  import { AIProviderName } from "../constants/enums.js";
2
- import type { NeurolinkCredentials, ProviderDescriptor } from "../types/index.js";
2
+ import type { DecideCatalogJson, NeurolinkCredentials, ProviderDescriptor } from "../types/index.js";
3
+ /**
4
+ * Builds the ProviderDescriptor of a JSON `decide` provider. It serves only
5
+ * `decide`, so it declares no auto-select, preference or health-sweep rank:
6
+ * those feed generation fallback chains a text-less provider must never join.
7
+ */
8
+ export declare function buildDecideDescriptor(entry: DecideCatalogJson): ProviderDescriptor;
3
9
  /**
4
10
  * Single source of truth for provider identity, credentials, defaults, and
5
11
  * runtime behavior classification — the hand-maintained providers plus the
@@ -14,10 +20,18 @@ export declare const PROVIDER_DESCRIPTORS_BY_NAME: ReadonlyMap<AIProviderName, P
14
20
  * ProviderFactory.normalizeProviderName().
15
21
  */
16
22
  export declare const PROVIDER_ALIAS_INDEX: ReadonlyMap<string, AIProviderName>;
23
+ /**
24
+ * Throws unless every descriptor that serves `decide` has its own finite
25
+ * `decideOrder`. The order is the precedence of resolveDefaultDecisionProvider(),
26
+ * so a missing or repeated number would make that choice depend on array position.
27
+ */
28
+ export declare function assertUniqueDecideOrder(descriptors: readonly ProviderDescriptor[]): void;
29
+ /** The descriptors that serve `decide`, lowest `decideOrder` first. */
30
+ export declare function orderDecisionProviders(descriptors: readonly ProviderDescriptor[]): readonly ProviderDescriptor[];
17
31
  /**
18
32
  * Providers that serve the `decide` inference type, derived from each
19
- * descriptor's `inferenceKinds`. A provider that declares nothing is a text
20
- * provider, so it is not here.
33
+ * descriptor's `inferenceKinds` and sorted by `decideOrder`, lowest first. A
34
+ * provider that declares nothing is a text provider, so it is not here.
21
35
  */
22
36
  export declare const DECISION_PROVIDERS: readonly ProviderDescriptor[];
23
37
  /**
@@ -1,7 +1,7 @@
1
1
  import { AIProviderName } from "../constants/enums.js";
2
- import { GoogleAIModels, OpenAIModels, AnthropicModels, VertexModels, OllamaModels, LiteLLMModels, NvidiaNimModels, OpenRouterModels, CohereModels, VoyageModels, TypeSafeModels, LayaModels, XorModels, PerplexityDeciderModels, CloudflareClefModels, JinaModels, StabilityModels, IdeogramModels, RecraftModels, ReplicateModels, } from "../constants/enums.js";
2
+ import { GoogleAIModels, OpenAIModels, AnthropicModels, VertexModels, OllamaModels, LiteLLMModels, NvidiaNimModels, OpenRouterModels, CohereModels, VoyageModels, TypeSafeModels, XorModels, PerplexityDeciderModels, CloudflareClefModels, JinaModels, StabilityModels, IdeogramModels, RecraftModels, ReplicateModels, } from "../constants/enums.js";
3
3
  import { API_KEY_FORMATS } from "../utils/providerConfig.js";
4
- import { getCatalogJsonEntries, catalogCredentialsKey, catalogEnvVar, } from "../providers/catalog/loader.js";
4
+ import { getCatalogJsonEntries, getDecideCatalogEntries, catalogCredentialsKey, catalogEnvVar, } from "../providers/catalog/loader.js";
5
5
  import { DEFAULT_INFERENCE_KINDS } from "../types/index.js";
6
6
  /**
7
7
  * Hand-maintained provider identity, credentials, defaults, and runtime
@@ -440,6 +440,7 @@ const HAND_DESCRIPTORS = [
440
440
  // what keeps it out of every generation code path, rather than each of
441
441
  // those having to special-case the provider name.
442
442
  inferenceKinds: ["decide"],
443
+ decideOrder: 1,
443
444
  // It emits no text, so it cannot call tools and cannot answer a
444
445
  // "live-generate" health probe.
445
446
  toolSupport: "none",
@@ -466,58 +467,13 @@ const HAND_DESCRIPTORS = [
466
467
  },
467
468
  setupUrl: "https://console.typesafe.ai/keys",
468
469
  },
469
- // Laya MUST stay after TypeSafe, XOR after Laya, Perplexity after XOR, and
470
- // Cloudflare Clef after Perplexity.
470
+ // Decision precedence is decideOrder, not position: typesafe 1, laya 2 (from
471
+ // catalog/laya.json), xor 3, perplexity-decider 4, cloudflare-clef 5.
471
472
  // resolveDefaultDecisionProvider() returns the first configured
472
- // DECISION_PROVIDERS entry, in this order, so a host configured for several
473
+ // DECISION_PROVIDERS entry in that order, so a host configured for several
473
474
  // keeps Jev for every built-in consumer and reaches the others only by
474
- // naming them. Reordering these silently changes which model routes,
475
- // compacts and plans for every such host.
476
- {
477
- name: AIProviderName.LAYA,
478
- aliases: [],
479
- credentialsKey: "laya",
480
- envVars: {
481
- apiKey: "LAYA_API_KEY",
482
- baseURL: "LAYA_BASE_URL",
483
- // Laya has no built-in endpoint (it is self-hosted or behind a proxy),
484
- // so a key is useless without a base URL. Both are required for it to
485
- // count as configured, from the environment or credentials.laya.
486
- extraRequired: ["LAYA_BASE_URL"],
487
- model: "LAYA_MODEL",
488
- },
489
- defaultModel: LayaModels.TYPED_DECISIONS,
490
- // Serves only `decide`, like TypeSafe — the one declaration that keeps it
491
- // out of every generation code path.
492
- inferenceKinds: ["decide"],
493
- toolSupport: "none",
494
- localRuntime: false,
495
- healthCheck: "env-only",
496
- // Deliberately NO autoSelectPriority / autoSelectPreference /
497
- // defaultHealthSweepPriority, for the same reason as TypeSafe above.
498
- timeouts: { decideMs: 5000 },
499
- // Laya's encoders cut the state off silently past their window (measured
500
- // live: a 60,000-character state answered 200 on 1,024 tokens). 768 is
501
- // the 1,024-token window minus the 256-token budget Laya reserves for a
502
- // question and its options; 320 is the English checkpoint's 512 minus
503
- // 192. `auto` may route to English, and an unlisted name (Laya's server
504
- // accepts aliases such as `en`) could be any checkpoint, so both get 320.
505
- // 64 is the question cap in Laya's own server. Non-ASCII rates were
506
- // measured live: Chinese is ~1.4 tokens per character on the English
507
- // tokenizer (typed-decisions, english) and ~0.56 on multilingual.
508
- decisionLimits: {
509
- maxStateTokens: 320,
510
- maxQuestions: 64,
511
- nonAsciiTokensPerChar: 1.5,
512
- models: {
513
- "typed-decisions": { maxStateTokens: 768 },
514
- multilingual: { maxStateTokens: 768, nonAsciiTokensPerChar: 0.6 },
515
- english: { maxStateTokens: 320 },
516
- auto: { maxStateTokens: 320 },
517
- },
518
- },
519
- setupUrl: "https://github.com/NandhaKishorM/laya",
520
- },
475
+ // naming them. Renumbering silently changes which model routes, compacts
476
+ // and plans for every such host.
521
477
  {
522
478
  name: AIProviderName.XOR,
523
479
  aliases: [],
@@ -535,6 +491,7 @@ const HAND_DESCRIPTORS = [
535
491
  // Serves only `decide`, like TypeSafe and Laya — the one declaration that
536
492
  // keeps it out of every generation code path.
537
493
  inferenceKinds: ["decide"],
494
+ decideOrder: 3,
538
495
  toolSupport: "none",
539
496
  localRuntime: false,
540
497
  healthCheck: "env-only",
@@ -579,6 +536,7 @@ const HAND_DESCRIPTORS = [
579
536
  // declaration that keeps it out of every generation code path, and out of
580
537
  // the way of the `perplexity` text provider.
581
538
  inferenceKinds: ["decide"],
539
+ decideOrder: 4,
582
540
  toolSupport: "none",
583
541
  localRuntime: false,
584
542
  healthCheck: "env-only",
@@ -646,6 +604,7 @@ const HAND_DESCRIPTORS = [
646
604
  // declaration that keeps it out of every generation code path, and out of
647
605
  // the way of the `cloudflare` text provider.
648
606
  inferenceKinds: ["decide"],
607
+ decideOrder: 5,
649
608
  toolSupport: "none",
650
609
  localRuntime: false,
651
610
  healthCheck: "env-only",
@@ -778,8 +737,48 @@ function buildCatalogDescriptor(entry) {
778
737
  : {}),
779
738
  };
780
739
  }
740
+ /**
741
+ * Builds the ProviderDescriptor of a JSON `decide` provider. It serves only
742
+ * `decide`, so it declares no auto-select, preference or health-sweep rank:
743
+ * those feed generation fallback chains a text-less provider must never join.
744
+ */
745
+ export function buildDecideDescriptor(entry) {
746
+ const fallbacks = entry.wire.apiKeyFallbackEnvVars?.length
747
+ ? { fallbacks: [...entry.wire.apiKeyFallbackEnvVars] }
748
+ : {};
749
+ return {
750
+ name: entry.id,
751
+ aliases: entry.aliases,
752
+ credentialsKey: catalogCredentialsKey(entry),
753
+ envVars: {
754
+ apiKey: catalogEnvVar(entry, "apiKey"),
755
+ ...fallbacks,
756
+ baseURL: catalogEnvVar(entry, "baseURL"),
757
+ // A key is useless without an endpoint when the provider has no built-in one.
758
+ ...(entry.decide.baseURLRequired
759
+ ? { extraRequired: [catalogEnvVar(entry, "baseURL")] }
760
+ : {}),
761
+ model: catalogEnvVar(entry, "model"),
762
+ },
763
+ defaultModel: entry.models.default,
764
+ inferenceKinds: ["decide"],
765
+ decideOrder: entry.decide.order,
766
+ toolSupport: "none",
767
+ localRuntime: false,
768
+ healthCheck: "env-only",
769
+ setupUrl: entry.setup.url,
770
+ ...(entry.setup.apiKeyFormat
771
+ ? { apiKeyFormatPattern: new RegExp(entry.setup.apiKeyFormat) }
772
+ : {}),
773
+ ...(entry.timeouts ? { timeouts: { ...entry.timeouts } } : {}),
774
+ ...(entry.decisionLimits ? { decisionLimits: entry.decisionLimits } : {}),
775
+ };
776
+ }
781
777
  function buildCatalogDescriptors() {
782
- return getCatalogJsonEntries().map(buildCatalogDescriptor);
778
+ return [
779
+ ...getCatalogJsonEntries().map(buildCatalogDescriptor),
780
+ ...getDecideCatalogEntries().map(buildDecideDescriptor),
781
+ ];
783
782
  }
784
783
  /**
785
784
  * Single source of truth for provider identity, credentials, defaults, and
@@ -801,12 +800,38 @@ export const PROVIDER_ALIAS_INDEX = new Map(PROVIDER_DESCRIPTORS.flatMap((d) =>
801
800
  [d.name.toLowerCase(), d.name],
802
801
  ...d.aliases.map((alias) => [alias.toLowerCase(), d.name]),
803
802
  ]));
803
+ const servesDecide = (descriptor) => (descriptor.inferenceKinds ?? DEFAULT_INFERENCE_KINDS).includes("decide");
804
+ /**
805
+ * Throws unless every descriptor that serves `decide` has its own finite
806
+ * `decideOrder`. The order is the precedence of resolveDefaultDecisionProvider(),
807
+ * so a missing or repeated number would make that choice depend on array position.
808
+ */
809
+ export function assertUniqueDecideOrder(descriptors) {
810
+ const owners = new Map();
811
+ for (const descriptor of descriptors.filter(servesDecide)) {
812
+ const order = descriptor.decideOrder;
813
+ if (order === undefined || !Number.isFinite(order)) {
814
+ throw new Error(`decision provider "${descriptor.name}" has no usable decideOrder`);
815
+ }
816
+ const clash = owners.get(order);
817
+ if (clash !== undefined) {
818
+ throw new Error(`decideOrder ${order} is used by both "${clash}" and "${descriptor.name}"`);
819
+ }
820
+ owners.set(order, descriptor.name);
821
+ }
822
+ }
823
+ /** The descriptors that serve `decide`, lowest `decideOrder` first. */
824
+ export function orderDecisionProviders(descriptors) {
825
+ const rank = (d) => d.decideOrder ?? Number.MAX_SAFE_INTEGER;
826
+ return descriptors.filter(servesDecide).sort((a, b) => rank(a) - rank(b));
827
+ }
828
+ assertUniqueDecideOrder(PROVIDER_DESCRIPTORS);
804
829
  /**
805
830
  * Providers that serve the `decide` inference type, derived from each
806
- * descriptor's `inferenceKinds`. A provider that declares nothing is a text
807
- * provider, so it is not here.
831
+ * descriptor's `inferenceKinds` and sorted by `decideOrder`, lowest first. A
832
+ * provider that declares nothing is a text provider, so it is not here.
808
833
  */
809
- export const DECISION_PROVIDERS = PROVIDER_DESCRIPTORS.filter((d) => (d.inferenceKinds ?? DEFAULT_INFERENCE_KINDS).includes("decide"));
834
+ export const DECISION_PROVIDERS = orderDecisionProviders(PROVIDER_DESCRIPTORS);
810
835
  /**
811
836
  * Whether a decision provider is fully configured: its key and every
812
837
  * extraRequired variable are set, each either in the environment or in
@@ -1,4 +1,12 @@
1
- import type { ProviderRegistryOptions } from "../types/index.js";
1
+ import type { DecideCatalogJson, ProviderRegistryOptions } from "../types/index.js";
2
+ /**
3
+ * Registers the config-driven `decide` providers described by catalog JSON
4
+ * (inferenceKinds ["decide"]). Each one is built by ConfiguredDecisionProvider,
5
+ * loaded lazily like every other provider; its descriptor declares
6
+ * inferenceKinds ["decide"], so nothing in the generation fallback chain can
7
+ * reach it.
8
+ */
9
+ export declare function registerDecideCatalogProviders(entries: readonly DecideCatalogJson[]): void;
2
10
  /**
3
11
  * Provider Registry - registers all providers with the factory
4
12
  * This is where we migrate providers one by one to the new pattern
@@ -1,9 +1,25 @@
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, LayaModels, XorModels, PerplexityDeciderModels, CloudflareClefModels, ReplicateModels, } from "../constants/enums.js";
3
+ import { AIProviderName, GoogleAIModels, OpenAIModels, AnthropicModels, VertexModels, OllamaModels, LiteLLMModels, NvidiaNimModels, OpenRouterModels, CohereModels, VoyageModels, JinaModels, StabilityModels, IdeogramModels, RecraftModels, TypeSafeModels, XorModels, PerplexityDeciderModels, CloudflareClefModels, 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";
7
+ import { catalogEnvVar, getDecideCatalogEntries, } from "../providers/catalog/loader.js";
8
+ /**
9
+ * Registers the config-driven `decide` providers described by catalog JSON
10
+ * (inferenceKinds ["decide"]). Each one is built by ConfiguredDecisionProvider,
11
+ * loaded lazily like every other provider; its descriptor declares
12
+ * inferenceKinds ["decide"], so nothing in the generation fallback chain can
13
+ * reach it.
14
+ */
15
+ export function registerDecideCatalogProviders(entries) {
16
+ for (const entry of entries) {
17
+ ProviderFactory.registerProvider(entry.id, async (modelName, _providerName, sdk, _region, credentials) => {
18
+ const { ConfiguredDecisionProvider } = await import("../providers/configuredDecision.js");
19
+ return new ConfiguredDecisionProvider(entry, modelName, sdk, credentials);
20
+ }, process.env[catalogEnvVar(entry, "model")] || entry.models.default, [...entry.aliases], PROVIDER_DESCRIPTORS_BY_NAME.get(entry.id));
21
+ }
22
+ }
7
23
  /**
8
24
  * Provider Registry - registers all providers with the factory
9
25
  * This is where we migrate providers one by one to the new pattern
@@ -234,14 +250,6 @@ export class ProviderRegistry {
234
250
  const { TypeSafeProvider } = await import("../providers/typesafe.js");
235
251
  return new TypeSafeProvider(modelName, sdk, undefined, typesafeCreds);
236
252
  }, 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));
245
253
  // Register XOR — the third `decide` provider, reached at whatever base
246
254
  // URL its config names. Its descriptor declares inferenceKinds:
247
255
  // ["decide"], so nothing in the generation fallback chain can reach it.
@@ -269,6 +277,8 @@ export class ProviderRegistry {
269
277
  const { CloudflareClefProvider } = await import("../providers/cloudflareClef.js");
270
278
  return new CloudflareClefProvider(modelName, sdk, undefined, cloudflareClefCreds);
271
279
  }, process.env.CLOUDFLARE_CLEF_MODEL || CloudflareClefModels.CLEF, [], PROVIDER_DESCRIPTORS_BY_NAME.get(AIProviderName.CLOUDFLARE_CLEF));
280
+ // The decide providers described by catalog JSON (laya.json and later).
281
+ registerDecideCatalogProviders(getDecideCatalogEntries());
272
282
  logger.debug("All AI providers registered successfully");
273
283
  // ===== MEDIA HANDLER REGISTRATION =====
274
284
  // Single registration path (Task 11): each ecosystem barrel (voice,
@@ -1,5 +1,5 @@
1
1
  import { PROVIDER_MAX_TOKENS } from "../core/constants.js";
2
- import { getCatalogJsonEntries } from "../providers/catalog/loader.js";
2
+ import { getCatalogJsonEntries, getDecideCatalogEntries, } from "../providers/catalog/loader.js";
3
3
  import { anthropicManifest } from "./manifests/anthropic.js";
4
4
  import { openaiManifest } from "./manifests/openai.js";
5
5
  import { azureManifest } from "./manifests/azure.js";
@@ -20,7 +20,6 @@ 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";
24
23
  import { xorManifest } from "./manifests/xor.js";
25
24
  import { perplexityDeciderManifest } from "./manifests/perplexityDecider.js";
26
25
  import { cloudflareClefManifest } from "./manifests/cloudflareClef.js";
@@ -94,6 +93,44 @@ const catalogManifest = (id) => {
94
93
  }
95
94
  return buildCatalogManifest(entry);
96
95
  };
96
+ /**
97
+ * Builds a manifest for a decide catalog provider from its catalog entry.
98
+ *
99
+ * A decide model emits no text, but the manifest type requires a
100
+ * `maxOutputTokens`, so the figure a decide entry declares is carried over
101
+ * as is. A model that omits `contextWindow` or `maxOutputTokens` takes the
102
+ * default model's, and `_default` is the default model's own row.
103
+ * `functionCalling` is always false: a decide model has no tool calling.
104
+ * No row gets a `displayName`: the decide catalog has no field for one and
105
+ * no reader shows a decide model's manifest name.
106
+ */
107
+ function buildDecideManifest(entry) {
108
+ const base = entry.models.catalog[entry.models.default];
109
+ if (base === undefined ||
110
+ base.contextWindow === undefined ||
111
+ base.maxOutputTokens === undefined) {
112
+ throw new Error(`Decide catalog entry "${entry.id}": models.default needs contextWindow and maxOutputTokens for its manifest`);
113
+ }
114
+ const baseWindow = base.contextWindow;
115
+ const baseMaxOutput = base.maxOutputTokens;
116
+ const toEntry = (spec) => ({
117
+ aliases: [],
118
+ contextWindow: spec.contextWindow ?? baseWindow,
119
+ maxOutputTokens: spec.maxOutputTokens ?? baseMaxOutput,
120
+ vision: spec.vision,
121
+ functionCalling: false,
122
+ });
123
+ return {
124
+ defaultContextWindow: baseWindow,
125
+ models: {
126
+ _default: toEntry(base),
127
+ ...Object.fromEntries(Object.entries(entry.models.catalog).map(([modelId, spec]) => [
128
+ modelId,
129
+ toEntry(spec),
130
+ ])),
131
+ },
132
+ };
133
+ }
97
134
  /**
98
135
  * Every provider's model manifest, keyed by the exact AIProviderName enum
99
136
  * value (kebab-case) — e.g. "google-ai", "nvidia-nim". Manifests are pure
@@ -122,7 +159,7 @@ export const MANIFEST_REGISTRY = {
122
159
  xai: xaiManifest,
123
160
  groq: groqManifest,
124
161
  typesafe: typesafeManifest,
125
- laya: layaManifest,
162
+ ...Object.fromEntries(getDecideCatalogEntries().map((e) => [e.id, buildDecideManifest(e)])),
126
163
  xor: xorManifest,
127
164
  "perplexity-decider": perplexityDeciderManifest,
128
165
  "cloudflare-clef": cloudflareClefManifest,