@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
@@ -6,7 +6,7 @@
6
6
  * Uses real MCP infrastructure for tool discovery and execution.
7
7
  */
8
8
  import type { DecisionCallerOptions, DecisionLimitsQuery, DecisionLimitsReading, DecisionOptions, DecisionResult, AgentDefinition, AgentNetworkConfig, AgentRunOptions, AgentRunOutcome, AgentToolRegistrationOptions, IsolatedAgentDefinition, NetworkExecutionInput, NetworkExecutionOptions, NetworkExecutionResult, NetworkStreamChunk, WorkerInstanceOptions } from "./types/index.js";
9
- import type { CompactionConfig, CompactionResult, ArtifactStore, SpanData, ObservabilityConfig, MetricsSummary, MCPToolAnnotations, TraceView, AuthenticatedContext, AuthProvider, JsonObject, NeuroLinkEvents, TypedEventEmitter, MCPEnhancementsConfig, NeuroLinkAuthConfig, NeurolinkConstructorConfig, ChatMessage, ExternalMCPOperationResult, ExternalMCPServerInstance, ExternalMCPServerHealth, ExternalMCPToolInfo, GenerateOptions, GenerateResult, ProviderStatus, TextGenerationOptions, TextGenerationResult, MCPExecutableTool, MCPServerInfo, MCPStatus, StreamOptions, StreamResult, ToolExecutionContext, ToolExecutionSummary, ToolInfo, ToolRegistrationOptions, BatchOperationResult, StreamGenerationEndContext, ToolRoutingServerDescriptor, ToolDedupConfig, ToolConfig, KnowledgeEngineStatus, ChecklistState, ArtifactPageRequest, BankArtifactOptions, BankedArtifactRef, DelegateCollectRequest, DelegateCollectResult, DelegateHandle, DelegateRegistrationOptions, DelegateSpawnOptions, BackgroundCommandHandle, BackgroundCommandOptions, BackgroundCommandOutputPage, BackgroundCommandPageRequest, BackgroundCommandPolicy, BackgroundCommandStatus, GitToolResult, GitToolsetOptions } from "./types/index.js";
9
+ import type { CompactionConfig, CompactionResult, ArtifactStore, SpanData, ObservabilityConfig, MetricsSummary, MCPToolAnnotations, TraceView, AuthenticatedContext, AuthProvider, JsonObject, NeuroLinkEvents, TypedEventEmitter, MCPEnhancementsConfig, NeuroLinkAuthConfig, NeurolinkConstructorConfig, ChatMessage, ConversationSeedMessage, ConversationSeedOptions, RecordConversationTurnOptions, RecordConversationTurnResult, ExternalMCPOperationResult, ExternalMCPServerInstance, ExternalMCPServerHealth, ExternalMCPToolInfo, GenerateOptions, GenerateResult, ProviderStatus, TextGenerationOptions, TextGenerationResult, MCPExecutableTool, MCPServerInfo, MCPStatus, StreamOptions, StreamResult, ToolExecutionContext, ToolExecutionSummary, ToolInfo, ToolRegistrationOptions, BatchOperationResult, StreamGenerationEndContext, ToolRoutingServerDescriptor, ToolDedupConfig, ToolConfig, KnowledgeEngineStatus, ChecklistState, ArtifactPageRequest, BankArtifactOptions, BankedArtifactRef, DelegateCollectRequest, DelegateCollectResult, DelegateHandle, DelegateRegistrationOptions, DelegateSpawnOptions, BackgroundCommandHandle, BackgroundCommandOptions, BackgroundCommandOutputPage, BackgroundCommandPageRequest, BackgroundCommandPolicy, BackgroundCommandStatus, GitToolResult, GitToolsetOptions } from "./types/index.js";
10
10
  import { ConversationMemoryManager } from "./core/conversationMemoryManager.js";
11
11
  import type { RedisConversationMemoryManager } from "./core/redisConversationMemoryManager.js";
12
12
  import { ExternalServerManager } from "./mcp/externalServerManager.js";
@@ -2120,6 +2120,57 @@ export declare class NeuroLink {
2120
2120
  * @returns Array of ChatMessage objects, or empty array if session doesn't exist
2121
2121
  */
2122
2122
  getSessionMessages(sessionId: string, userId?: string): Promise<ChatMessage[]>;
2123
+ /**
2124
+ * Record one finished turn from a transport that does not go through
2125
+ * generate()/stream() — a voice call — into the same conversation memory
2126
+ * typed chat uses: same key, same manager, same summarization and title
2127
+ * handling, same optional long-term memory write.
2128
+ *
2129
+ * Identity is never defaulted: `userId` is required, because typed chat falls
2130
+ * back to a shared placeholder user when it is missing and a spoken turn must
2131
+ * never be filed there. Recording the same `turnId` twice stores it once, so
2132
+ * a caller can retry after an ambiguous failure.
2133
+ *
2134
+ * A turn with a blank user message, or a blank response and no tool events,
2135
+ * is not stored (an empty assistant message makes some providers reject the
2136
+ * next request); the result says which. Storage failures throw — the caller
2137
+ * decides whether to retry or to carry on without the turn.
2138
+ *
2139
+ * @throws NeuroLinkError (INVALID_PARAMETERS) for a bad sessionId or userId
2140
+ * @throws NeuroLinkError (MISSING_CONFIGURATION) when conversation memory is not enabled
2141
+ * @throws ConversationMemoryError (STORAGE_ERROR) when the store rejects the write
2142
+ */
2143
+ recordConversationTurn(options: RecordConversationTurnOptions): Promise<RecordConversationTurnResult>;
2144
+ /**
2145
+ * The most recent user/assistant messages of a conversation, bounded, for
2146
+ * seeding a model that has no history of its own — a voice model at call
2147
+ * start. Summaries, pinned skills, tool traffic and blank messages are left
2148
+ * out; the oldest messages are dropped first when `maxChars` would be
2149
+ * exceeded. Best effort: a missing conversation, disabled memory or a read
2150
+ * failure yields an empty seed and never throws, because a call must start
2151
+ * either way.
2152
+ */
2153
+ getConversationSeed(options: ConversationSeedOptions): Promise<ConversationSeedMessage[]>;
2154
+ /**
2155
+ * Whether a conversation already holds messages under this user. A client
2156
+ * may name the conversation a call appends to; the server accepts the name
2157
+ * only when this is true for the user the session is verified as.
2158
+ */
2159
+ conversationExists(options: {
2160
+ sessionId: string;
2161
+ userId: string;
2162
+ }): Promise<boolean>;
2163
+ /**
2164
+ * Whether a stored conversation is owned by someone other than `userId`.
2165
+ *
2166
+ * The Redis manager includes the user in the key, so a different user simply
2167
+ * never finds the conversation. The in-process manager keys sessions by id
2168
+ * alone and only records the owner, so ownership is checked here: a session
2169
+ * that exists with no recorded owner, or another one, is not `userId`'s.
2170
+ */
2171
+ private isOwnedByAnotherUser;
2172
+ /** Shared identity check for the voice-memory methods. */
2173
+ private static assertConversationIdentity;
2123
2174
  /**
2124
2175
  * Replace the entire messages array for a session.
2125
2176
  * @param sessionId - The session ID to update
package/dist/neurolink.js CHANGED
@@ -9342,6 +9342,9 @@ Current user's request: ${currentInput}`;
9342
9342
  ...(pendingSkillMessages.length > 0
9343
9343
  ? { skillMessages: pendingSkillMessages }
9344
9344
  : {}),
9345
+ ...(enhancedOptions.modality
9346
+ ? { modality: enhancedOptions.modality }
9347
+ : {}),
9345
9348
  });
9346
9349
  this.recordMemorySpan("memory.store", { "memory.operation": "store", "memory.path": "stream" }, Date.now() - memStoreStart, SpanStatus.OK);
9347
9350
  logger.debug("[NeuroLink.stream] Stored conversation turn with events", {
@@ -12757,6 +12760,198 @@ Current user's request: ${currentInput}`;
12757
12760
  }
12758
12761
  return await this.conversationMemory.getSessionMessages(sessionId, userId);
12759
12762
  }
12763
+ /**
12764
+ * Record one finished turn from a transport that does not go through
12765
+ * generate()/stream() — a voice call — into the same conversation memory
12766
+ * typed chat uses: same key, same manager, same summarization and title
12767
+ * handling, same optional long-term memory write.
12768
+ *
12769
+ * Identity is never defaulted: `userId` is required, because typed chat falls
12770
+ * back to a shared placeholder user when it is missing and a spoken turn must
12771
+ * never be filed there. Recording the same `turnId` twice stores it once, so
12772
+ * a caller can retry after an ambiguous failure.
12773
+ *
12774
+ * A turn with a blank user message, or a blank response and no tool events,
12775
+ * is not stored (an empty assistant message makes some providers reject the
12776
+ * next request); the result says which. Storage failures throw — the caller
12777
+ * decides whether to retry or to carry on without the turn.
12778
+ *
12779
+ * @throws NeuroLinkError (INVALID_PARAMETERS) for a bad sessionId or userId
12780
+ * @throws NeuroLinkError (MISSING_CONFIGURATION) when conversation memory is not enabled
12781
+ * @throws ConversationMemoryError (STORAGE_ERROR) when the store rejects the write
12782
+ */
12783
+ async recordConversationTurn(options) {
12784
+ const { sessionId, userId, turnId } = options;
12785
+ NeuroLink.assertConversationIdentity(sessionId, userId);
12786
+ const identity = { sessionId, userId, ...(turnId ? { turnId } : {}) };
12787
+ if (!options.userMessage.trim()) {
12788
+ return { recorded: false, reason: "empty-user-message", ...identity };
12789
+ }
12790
+ const hasToolEvents = (options.events ?? []).some((event) => event.type === "tool:start" || event.type === "tool:end");
12791
+ if (!options.aiResponse.trim() && !hasToolEvents) {
12792
+ return { recorded: false, reason: "empty-response", ...identity };
12793
+ }
12794
+ await this.initializeConversationMemoryForGeneration(options.requestId ?? `record-turn-${Date.now()}`, Date.now(), process.hrtime.bigint());
12795
+ if (!this.conversationMemory) {
12796
+ throw new NeuroLinkError({
12797
+ code: ERROR_CODES.MISSING_CONFIGURATION,
12798
+ message: "Conversation memory is not enabled",
12799
+ category: ErrorCategory.VALIDATION,
12800
+ severity: ErrorSeverity.HIGH,
12801
+ retriable: false,
12802
+ });
12803
+ }
12804
+ if (await this.isOwnedByAnotherUser(sessionId, userId)) {
12805
+ throw new NeuroLinkError({
12806
+ code: ERROR_CODES.INVALID_PARAMETERS,
12807
+ message: "The conversation belongs to another user",
12808
+ category: ErrorCategory.VALIDATION,
12809
+ severity: ErrorSeverity.MEDIUM,
12810
+ retriable: false,
12811
+ });
12812
+ }
12813
+ const memStoreStart = Date.now();
12814
+ try {
12815
+ await this.conversationMemory.storeConversationTurn({
12816
+ sessionId,
12817
+ userId,
12818
+ userMessage: options.userMessage,
12819
+ aiResponse: options.aiResponse,
12820
+ startTimeStamp: options.startTime,
12821
+ ...(options.provider && options.model
12822
+ ? {
12823
+ providerDetails: {
12824
+ provider: options.provider,
12825
+ model: options.model,
12826
+ },
12827
+ }
12828
+ : {}),
12829
+ enableSummarization: options.enableSummarization,
12830
+ events: options.events?.length ? options.events : undefined,
12831
+ requestId: options.requestId,
12832
+ ...(options.modality ? { modality: options.modality } : {}),
12833
+ ...(turnId ? { turnId } : {}),
12834
+ ...(options.interrupted ? { interrupted: true } : {}),
12835
+ ...(options.callEnded ? { callEnded: true } : {}),
12836
+ });
12837
+ this.recordMemorySpan("memory.store", { "memory.operation": "store", "memory.path": "record" }, Date.now() - memStoreStart, SpanStatus.OK);
12838
+ }
12839
+ catch (error) {
12840
+ this.recordMemorySpan("memory.store", { "memory.operation": "store", "memory.path": "record" }, Date.now() - memStoreStart, SpanStatus.ERROR, error instanceof Error ? error.message : String(error));
12841
+ throw error;
12842
+ }
12843
+ if (this.shouldWriteMemory(options.memory, userId, options.aiResponse)) {
12844
+ this.storeMemoryInBackground({
12845
+ prompt: options.userMessage,
12846
+ response: options.aiResponse.trim(),
12847
+ userId,
12848
+ sessionId,
12849
+ provider: options.provider ?? "",
12850
+ model: options.model,
12851
+ }, options.memory, { sessionId });
12852
+ }
12853
+ return { recorded: true, ...identity };
12854
+ }
12855
+ /**
12856
+ * The most recent user/assistant messages of a conversation, bounded, for
12857
+ * seeding a model that has no history of its own — a voice model at call
12858
+ * start. Summaries, pinned skills, tool traffic and blank messages are left
12859
+ * out; the oldest messages are dropped first when `maxChars` would be
12860
+ * exceeded. Best effort: a missing conversation, disabled memory or a read
12861
+ * failure yields an empty seed and never throws, because a call must start
12862
+ * either way.
12863
+ */
12864
+ async getConversationSeed(options) {
12865
+ const { sessionId, userId } = options;
12866
+ const maxTurns = Math.max(0, Math.floor(options.maxTurns ?? 10));
12867
+ const maxChars = Math.max(0, Math.floor(options.maxChars ?? 6000));
12868
+ try {
12869
+ NeuroLink.assertConversationIdentity(sessionId, userId);
12870
+ const stored = await this.getSessionMessages(sessionId, userId);
12871
+ const spoken = [];
12872
+ for (const message of stored) {
12873
+ if ((message.role !== "user" && message.role !== "assistant") ||
12874
+ message.metadata?.isSummary ||
12875
+ message.metadata?.isSkill ||
12876
+ typeof message.content !== "string" ||
12877
+ !message.content.trim()) {
12878
+ continue;
12879
+ }
12880
+ spoken.push({
12881
+ role: message.role,
12882
+ content: message.content,
12883
+ ...(message.metadata?.modality
12884
+ ? { modality: message.metadata.modality }
12885
+ : {}),
12886
+ });
12887
+ }
12888
+ const recent = spoken.slice(-(maxTurns * 2));
12889
+ let used = 0;
12890
+ let firstKept = recent.length;
12891
+ for (let index = recent.length - 1; index >= 0; index -= 1) {
12892
+ used += recent[index].content.length;
12893
+ if (used > maxChars) {
12894
+ break;
12895
+ }
12896
+ firstKept = index;
12897
+ }
12898
+ return recent.slice(firstKept);
12899
+ }
12900
+ catch (error) {
12901
+ logger.warn("[NeuroLink] getConversationSeed failed, starting unseeded", {
12902
+ error: error instanceof Error ? error.message : String(error),
12903
+ });
12904
+ return [];
12905
+ }
12906
+ }
12907
+ /**
12908
+ * Whether a conversation already holds messages under this user. A client
12909
+ * may name the conversation a call appends to; the server accepts the name
12910
+ * only when this is true for the user the session is verified as.
12911
+ */
12912
+ async conversationExists(options) {
12913
+ NeuroLink.assertConversationIdentity(options.sessionId, options.userId);
12914
+ const stored = await this.getSessionMessages(options.sessionId, options.userId);
12915
+ if (stored.length === 0) {
12916
+ return false;
12917
+ }
12918
+ return !(await this.isOwnedByAnotherUser(options.sessionId, options.userId));
12919
+ }
12920
+ /**
12921
+ * Whether a stored conversation is owned by someone other than `userId`.
12922
+ *
12923
+ * The Redis manager includes the user in the key, so a different user simply
12924
+ * never finds the conversation. The in-process manager keys sessions by id
12925
+ * alone and only records the owner, so ownership is checked here: a session
12926
+ * that exists with no recorded owner, or another one, is not `userId`'s.
12927
+ */
12928
+ async isOwnedByAnotherUser(sessionId, userId) {
12929
+ const memory = this.conversationMemory;
12930
+ if (!memory || memory.isRedisConfigured) {
12931
+ return false;
12932
+ }
12933
+ const session = await memory.getSession(sessionId);
12934
+ return session !== undefined && session.userId !== userId;
12935
+ }
12936
+ /** Shared identity check for the voice-memory methods. */
12937
+ static assertConversationIdentity(sessionId, userId) {
12938
+ const problem = typeof sessionId !== "string" || !sessionId.trim()
12939
+ ? "sessionId must be a non-empty string"
12940
+ : sessionId.includes(":")
12941
+ ? 'sessionId must not contain ":"'
12942
+ : typeof userId !== "string" || !userId.trim()
12943
+ ? "userId must be a non-empty string"
12944
+ : undefined;
12945
+ if (problem) {
12946
+ throw new NeuroLinkError({
12947
+ code: ERROR_CODES.INVALID_PARAMETERS,
12948
+ message: problem,
12949
+ category: ErrorCategory.VALIDATION,
12950
+ severity: ErrorSeverity.MEDIUM,
12951
+ retriable: false,
12952
+ });
12953
+ }
12954
+ }
12760
12955
  /**
12761
12956
  * Replace the entire messages array for a session.
12762
12957
  * @param sessionId - The session ID to update
@@ -1,3 +1,5 @@
1
- import type { ProviderCatalogJson } from "../../types/index.js";
1
+ import type { DecideCatalogJson, ProviderCatalogJson } from "../../types/index.js";
2
2
  export declare const CATALOG_JSON_ENTRIES: ProviderCatalogJson[];
3
3
  export declare const CATALOG_PROVIDER_IDS: readonly ["a2agent", "abliteration", "above-dev", "ai21", "aiand", "aihubmix", "aionlabs", "ambient", "api-route", "arcee", "atlas-cloud", "avian-io", "baidu-qianfan", "baseten", "bee-heossi", "byteplus-modelark", "bytez", "cerebras", "charm-hyper", "chutes", "cloudflare", "crusoe", "dashscope", "deepinfra", "deepseek", "empiriolabs", "featherless-ai", "fireworks", "friendli", "gmicloud", "gradientai", "groq", "hetzner-inference", "huggingface", "inception-labs", "inceptron", "inco", "inference-net", "io-intelligence", "koscompute", "lemonfox-ai", "lilac", "llmtech", "mancer", "meta-model-api", "minimax", "mistral", "moark", "modelscope", "moonshot-ai", "morph", "naver-clova-studio", "nebius", "neuralwatt", "novita", "ovhcloud", "parasail", "pareto-inference", "perplexity", "poolside", "prime-intellect", "reka", "sakana-ai", "sambanova", "sarvam", "scaleway", "siliconflow", "stackit", "stepfun", "subconscious", "synthetic", "telnyx", "thinking-machines", "tinfoil", "together-ai", "umans-ai", "upstage", "venice-ai", "vispark", "vultr-inference", "wafer", "wandb-inference", "xai", "z-ai"];
4
+ export declare const DECIDE_CATALOG_JSON_ENTRIES: DecideCatalogJson[];
5
+ export declare const DECIDE_CATALOG_PROVIDER_IDS: readonly ["laya"];
@@ -40,6 +40,7 @@ import incoJson from "./inco.json" with { type: "json" };
40
40
  import inferenceNetJson from "./inference-net.json" with { type: "json" };
41
41
  import ioIntelligenceJson from "./io-intelligence.json" with { type: "json" };
42
42
  import koscomputeJson from "./koscompute.json" with { type: "json" };
43
+ import layaJson from "./laya.json" with { type: "json" };
43
44
  import lemonfoxAiJson from "./lemonfox-ai.json" with { type: "json" };
44
45
  import lilacJson from "./lilac.json" with { type: "json" };
45
46
  import llmtechJson from "./llmtech.json" with { type: "json" };
@@ -84,6 +85,14 @@ import waferJson from "./wafer.json" with { type: "json" };
84
85
  import wandbInferenceJson from "./wandb-inference.json" with { type: "json" };
85
86
  import xaiJson from "./xai.json" with { type: "json" };
86
87
  import zAiJson from "./z-ai.json" with { type: "json" };
88
+ import { isDecideCatalogEntry, parseCatalogEntry } from "./schema.js";
89
+ const asDecideEntry = (raw) => {
90
+ const entry = parseCatalogEntry(raw);
91
+ if (!isDecideCatalogEntry(entry)) {
92
+ throw new Error("a catalog file listed as a decide entry parsed as a chat entry");
93
+ }
94
+ return entry;
95
+ };
87
96
  export const CATALOG_JSON_ENTRIES = [
88
97
  a2agentJson,
89
98
  abliterationJson,
@@ -256,3 +265,5 @@ export const CATALOG_PROVIDER_IDS = [
256
265
  "xai",
257
266
  "z-ai",
258
267
  ];
268
+ export const DECIDE_CATALOG_JSON_ENTRIES = [layaJson].map(asDecideEntry);
269
+ export const DECIDE_CATALOG_PROVIDER_IDS = ["laya"];
@@ -0,0 +1,107 @@
1
+ {
2
+ "$schema": "./provider-catalog.schema.json",
3
+ "id": "laya",
4
+ "displayName": "Laya",
5
+ "aliases": [],
6
+ "inferenceKinds": ["decide"],
7
+ "wire": {},
8
+ "models": {
9
+ "default": "typed-decisions",
10
+ "topModels": ["typed-decisions", "multilingual", "english", "auto"],
11
+ "catalog": {
12
+ "typed-decisions": {
13
+ "contextWindow": 1024,
14
+ "maxOutputTokens": 256,
15
+ "vision": false,
16
+ "status": "production",
17
+ "description": "Recommended - Laya fine-tuned for typed decisions"
18
+ },
19
+ "english": {
20
+ "contextWindow": 512,
21
+ "maxOutputTokens": 256,
22
+ "vision": false,
23
+ "status": "production",
24
+ "description": "Laya, English base checkpoint (512-token window)"
25
+ },
26
+ "multilingual": {
27
+ "contextWindow": 1024,
28
+ "maxOutputTokens": 256,
29
+ "vision": false,
30
+ "status": "production",
31
+ "description": "Laya, 100+ languages"
32
+ },
33
+ "auto": {
34
+ "contextWindow": 512,
35
+ "maxOutputTokens": 256,
36
+ "vision": false,
37
+ "status": "production",
38
+ "description": "Let Laya pick a checkpoint by language"
39
+ }
40
+ }
41
+ },
42
+ "timeouts": { "decideMs": 5000 },
43
+ "decisionLimits": {
44
+ "maxStateTokens": 320,
45
+ "maxQuestions": 64,
46
+ "nonAsciiTokensPerChar": 1.5,
47
+ "models": {
48
+ "typed-decisions": { "maxStateTokens": 768 },
49
+ "multilingual": { "maxStateTokens": 768, "nonAsciiTokensPerChar": 0.6 },
50
+ "english": { "maxStateTokens": 320 },
51
+ "auto": { "maxStateTokens": 320 }
52
+ }
53
+ },
54
+ "decide": {
55
+ "dialect": "system-one",
56
+ "order": 2,
57
+ "baseURLRequired": true,
58
+ "baseURLNormalize": ["strip-trailing-slash"],
59
+ "endpoint": "{baseURL}/predict",
60
+ "model": { "placement": "body-unless-alias", "alias": "auto" },
61
+ "requestIdHeaders": ["x-litellm-call-id"],
62
+ "response": { "modelPaths": ["routing.model", "model"] },
63
+ "errors": {
64
+ "rules": [
65
+ { "status": [401, 403], "kind": "authentication" },
66
+ { "status": 413, "kind": "max_tokens_exceeded" },
67
+ { "status": 429, "kind": "rate_limit" },
68
+ { "status": 503, "kind": "overloaded" },
69
+ { "statusMin": 500, "kind": "server" }
70
+ ],
71
+ "default": "invalid_request",
72
+ "bodyShapes": [
73
+ { "path": "error.message" },
74
+ {
75
+ "path": "detail",
76
+ "isArray": true,
77
+ "formatter": "fastapi-validation",
78
+ "kind": "invalid_request",
79
+ "retryable": false
80
+ },
81
+ { "path": "detail" }
82
+ ]
83
+ }
84
+ },
85
+ "setup": {
86
+ "url": "https://github.com/NandhaKishorM/laya",
87
+ "apiKeyFormat": null,
88
+ "billingPolicy": "free-tier",
89
+ "description": "API key for your Laya endpoint",
90
+ "instructions": [
91
+ "1. Run a Laya server, or use a LiteLLM proxy with a pass-through route to one",
92
+ "2. Set LAYA_BASE_URL to its base URL (NeuroLink calls <LAYA_BASE_URL>/predict)",
93
+ "3. Set {apiKeyEnvVar} to the key it accepts (on LiteLLM, a virtual key with that route in its Allowed Routes)"
94
+ ]
95
+ },
96
+ "evidence": {
97
+ "rosterVerified": {
98
+ "date": "2026-09-24",
99
+ "method": "Checkpoint ids typed-decisions, multilingual and english as listed in docs/getting-started/providers/laya.md and answered by a live Laya 0.3.5 server (routing.model); auto sends no model and lets Laya's router choose"
100
+ },
101
+ "liveMatrix": {
102
+ "date": "2026-09-24",
103
+ "result": "A 60,000-character state was answered 200 on a 1,024-token window, so Laya truncates silently and decisionLimits refuses over-window input (768 state tokens on typed-decisions and multilingual, 320 on english and auto). Chinese measured about 1.4 tokens per character on the English tokenizer and 0.56 on multilingual."
104
+ },
105
+ "addedInPR": "https://github.com/juspay/neurolink/pull/1978"
106
+ }
107
+ }
@@ -1,6 +1,9 @@
1
- import type { OpenAICompatCatalogEntry, ProviderCatalogJson, ProviderConfigOptions } from "../../types/index.js";
2
- export declare function catalogCredentialsKey(entry: ProviderCatalogJson): string;
3
- export declare function catalogEnvVar(entry: ProviderCatalogJson, kind: "apiKey" | "baseURL" | "model"): string;
4
- export declare function buildCatalogConfigOptions(entry: ProviderCatalogJson): ProviderConfigOptions;
1
+ import type { CatalogEnvSource, CatalogKeySource, CatalogSetupSource, DecideCatalogJson, OpenAICompatCatalogEntry, ProviderCatalogJson, ProviderConfigOptions } from "../../types/index.js";
2
+ export declare function catalogCredentialsKey(entry: CatalogKeySource): string;
3
+ export declare function catalogEnvVar(entry: CatalogEnvSource, kind: "apiKey" | "baseURL" | "model"): string;
4
+ export declare function buildCatalogConfigOptions(entry: CatalogSetupSource): ProviderConfigOptions;
5
+ export declare function freezeCatalogCopy<T>(value: T): T;
5
6
  export declare function getCatalogJsonEntries(): readonly ProviderCatalogJson[];
7
+ /** The `decide` entries, copied and frozen exactly as getCatalogJsonEntries() does. */
8
+ export declare function getDecideCatalogEntries(): readonly DecideCatalogJson[];
6
9
  export declare function buildCatalogEntries(): OpenAICompatCatalogEntry[];
@@ -4,7 +4,7 @@
4
4
  * knowledge about deriving env vars, template interpolation and error
5
5
  * rule matching lives HERE — the JSON stays declarative.
6
6
  */
7
- import { CATALOG_JSON_ENTRIES } from "./index.generated.js";
7
+ import { CATALOG_JSON_ENTRIES, DECIDE_CATALOG_JSON_ENTRIES, } from "./index.generated.js";
8
8
  import { DEFAULT_ERROR_RULES } from "../../utils/errorClassifier.js";
9
9
  import { AuthenticationError, RateLimitError, InvalidModelError, NetworkError, ProviderError, } from "../../types/index.js";
10
10
  const ERROR_CLASS_MAP = {
@@ -18,7 +18,7 @@ const ERROR_CLASS_MAP = {
18
18
  // import from tools/ (build-time tooling depends on runtime source, not the
19
19
  // reverse).
20
20
  function toCamelCase(id) {
21
- return id.replace(/-([a-z])/g, (_m, c) => c.toUpperCase());
21
+ return id.replace(/-([a-z0-9])/g, (_m, c) => c.toUpperCase());
22
22
  }
23
23
  // Resolves the NeurolinkCredentials key for an entry — respects a
24
24
  // credentialsKey override (required where the derived camelCase would
@@ -108,8 +108,15 @@ function deepFreeze(value) {
108
108
  // CATALOG_JSON_ENTRIES's own objects), then deepFreeze() the clone. All
109
109
  // current callers only .find/.map/.flatMap/.filter/iterate, so this is
110
110
  // behavior-preserving for them.
111
+ export function freezeCatalogCopy(value) {
112
+ return deepFreeze(structuredClone(value));
113
+ }
111
114
  export function getCatalogJsonEntries() {
112
- return deepFreeze(structuredClone(CATALOG_JSON_ENTRIES));
115
+ return freezeCatalogCopy(CATALOG_JSON_ENTRIES);
116
+ }
117
+ /** The `decide` entries, copied and frozen exactly as getCatalogJsonEntries() does. */
118
+ export function getDecideCatalogEntries() {
119
+ return freezeCatalogCopy(DECIDE_CATALOG_JSON_ENTRIES);
113
120
  }
114
121
  export function buildCatalogEntries() {
115
122
  return CATALOG_JSON_ENTRIES.map((entry) => {