UNPKG

@juspay/neurolink

Version:

The pipe layer of an AI nervous system: one interface connecting provider neurons to your application, across three inference types — generate, stream and decide. `decide` returns typed, calibrated judgements from a non-generative model (~400ms, ~$0.00002

432 lines (431 loc) • 29.3 kB
import { ProviderFactory } from "./providerFactory.js"; import { logger } from "../utils/logger.js"; 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"; import { PROVIDER_DESCRIPTORS_BY_NAME } from "./providerDescriptors.js"; import { OPENAI_COMPAT_CATALOG } from "../providers/openaiCompatCatalog.js"; import { providerChoicesFor } from "./mediaHandlerCatalog.js"; /** * Provider Registry - registers all providers with the factory * This is where we migrate providers one by one to the new pattern */ export class ProviderRegistry { static registered = false; static registrationPromise = null; static options = { enableManualMCP: false, // Default to disabled for safety }; /** * NEW4: per-handler registration outcomes for the realtime voice * providers. `"ok"` = registered; any other string = the error message. * Empty until the first `registerAllProviders()` call. */ static realtimeRegistration = {}; /** * Returns a snapshot of voice provider registration outcomes so callers * can detect at runtime which voice handlers are usable. Useful in * health-check endpoints and CI startup probes. */ static getRegistrationReport() { return { realtime: { ...this.realtimeRegistration } }; } /** * Register all providers with the factory */ static async registerAllProviders() { if (this.registered) { return; } if (this.registrationPromise) { return this.registrationPromise; } this.registrationPromise = this._doRegister(); try { await this.registrationPromise; } catch (error) { this.registrationPromise = null; // Allow retry on failure throw error; } } /** * Internal registration implementation * * Flat list of ProviderFactory.registerProvider() calls. Providers load via * dynamic import() of ../providers/<module>.js only - never static imports * (avoids circular dependencies; see CLAUDE.md). * * Not registered (by design): index.ts, providerTypeUtils.ts, * anthropicBaseProvider.ts (legacy; anthropic.ts is live), * googleNativeGemini3.ts (shared helpers). Filename != provider ID * (e.g. amazonBedrock -> "bedrock"); static scanners that miss dynamic * imports may false-positive (Pattern Analysis #1178). */ // eslint-disable-next-line max-lines-per-function static async _doRegister() { try { // Register providers with dynamic import factory functions const { ProviderFactory } = await import("./providerFactory.js"); // Register Google AI Studio Provider (our validated baseline) ProviderFactory.registerProvider(AIProviderName.GOOGLE_AI, async (modelName, _providerName, sdk, _region, credentials) => { const googleAiCreds = credentials; const { GoogleAIStudioProvider } = await import("../providers/googleAiStudio/index.js"); return new GoogleAIStudioProvider(modelName, sdk, googleAiCreds); }, GoogleAIModels.GEMINI_2_5_FLASH, ["googleAiStudio", "google", "gemini", "google-ai", "google-ai-studio"], PROVIDER_DESCRIPTORS_BY_NAME.get(AIProviderName.GOOGLE_AI)); // Register OpenAI provider ProviderFactory.registerProvider(AIProviderName.OPENAI, async (modelName, _providerName, sdk, _region, credentials) => { const openaiCreds = credentials; const { OpenAIProvider } = await import("../providers/openAI/index.js"); return new OpenAIProvider(modelName, sdk, undefined, openaiCreds); }, OpenAIModels.GPT_4O_MINI, ["gpt", "chatgpt"], PROVIDER_DESCRIPTORS_BY_NAME.get(AIProviderName.OPENAI)); // Register Anthropic provider ProviderFactory.registerProvider(AIProviderName.ANTHROPIC, async (modelName, _providerName, sdk, _region, credentials) => { const anthropicCreds = credentials; const { AnthropicProvider } = await import("../providers/anthropic/index.js"); return new AnthropicProvider(modelName, sdk, undefined, anthropicCreds); }, AnthropicModels.CLAUDE_SONNET_4_6, ["claude", "anthropic"], PROVIDER_DESCRIPTORS_BY_NAME.get(AIProviderName.ANTHROPIC)); // Register Amazon Bedrock provider ProviderFactory.registerProvider(AIProviderName.BEDROCK, async (modelName, _providerName, sdk, region, credentials) => { const bedrockCreds = credentials; const { AmazonBedrockProvider } = await import("../providers/amazonBedrock/index.js"); return new AmazonBedrockProvider(modelName, sdk, region, bedrockCreds); }, undefined, // Let provider read BEDROCK_MODEL from .env ["bedrock", "aws"], PROVIDER_DESCRIPTORS_BY_NAME.get(AIProviderName.BEDROCK)); // Register Azure OpenAI provider ProviderFactory.registerProvider(AIProviderName.AZURE, async (modelName, _providerName, sdk, _region, credentials) => { const azureCreds = credentials; const { AzureOpenAIProvider } = await import("../providers/azureOpenai.js"); return new AzureOpenAIProvider(modelName, sdk, undefined, azureCreds); }, process.env.AZURE_MODEL || process.env.AZURE_OPENAI_MODEL || process.env.AZURE_OPENAI_DEPLOYMENT || process.env.AZURE_OPENAI_DEPLOYMENT_ID || "gpt-4o-mini", ["azure", "azureOpenai"], PROVIDER_DESCRIPTORS_BY_NAME.get(AIProviderName.AZURE)); // Register Google Vertex AI provider ProviderFactory.registerProvider(AIProviderName.VERTEX, async (modelName, providerName, sdk, region, credentials) => { const vertexCreds = credentials; const { GoogleVertexProvider } = await import("../providers/googleVertex/index.js"); return new GoogleVertexProvider(modelName, providerName, sdk, region, vertexCreds); }, VertexModels.CLAUDE_4_6_SONNET, ["vertex", "googleVertex"], PROVIDER_DESCRIPTORS_BY_NAME.get(AIProviderName.VERTEX)); // HuggingFace migrated to the JSON catalog (catalog/huggingface.json) // — it is now registered by the "config-driven OpenAI-compatible // catalog providers" loop below, not a dedicated block here. // Register the config-driven OpenAI-compatible catalog providers // (cerebras, groq, xai, together-ai, fireworks, perplexity, mistral, // cloudflare, deepseek, huggingface). // To add a new OpenAI-compatible provider, add one entry to // OPENAI_COMPAT_CATALOG (openaiCompatCatalog.ts) — not a new block here. for (const entry of OPENAI_COMPAT_CATALOG) { ProviderFactory.registerProvider(entry.providerName, async (modelName, _providerName, sdk, _region, credentials) => { const { ConfiguredOpenAICompatProvider } = await import("../providers/configuredOpenAICompat.js"); return new ConfiguredOpenAICompatProvider(entry, modelName, sdk, credentials); }, entry.registryDefaultModelChecksEnvVar ? process.env[entry.modelEnvVar] || entry.registryDefaultModel : entry.registryDefaultModel, entry.aliases, PROVIDER_DESCRIPTORS_BY_NAME.get(entry.providerName)); } // Register Ollama provider ProviderFactory.registerProvider(AIProviderName.OLLAMA, async (modelName, _providerName, sdk, _region, credentials) => { const ollamaCreds = credentials; const { OllamaProvider } = await import("../providers/ollama/index.js"); return new OllamaProvider(modelName, sdk, undefined, ollamaCreds); }, process.env.OLLAMA_MODEL || OllamaModels.LLAMA3_2_LATEST, ["ollama", "local"], PROVIDER_DESCRIPTORS_BY_NAME.get(AIProviderName.OLLAMA)); // Register LiteLLM provider ProviderFactory.registerProvider(AIProviderName.LITELLM, async (modelName, _providerName, sdk, _region, credentials) => { const litellmCreds = credentials; const { LiteLLMProvider } = await import("../providers/litellm/index.js"); return new LiteLLMProvider(modelName, sdk, undefined, litellmCreds); }, process.env.LITELLM_MODEL || LiteLLMModels.OPENAI_GPT_4O_MINI, ["litellm"], PROVIDER_DESCRIPTORS_BY_NAME.get(AIProviderName.LITELLM)); // Register OpenAI Compatible provider ProviderFactory.registerProvider(AIProviderName.OPENAI_COMPATIBLE, async (modelName, _providerName, sdk, _region, credentials) => { const openaiCompatCreds = credentials; const { OpenAICompatibleProvider } = await import("../providers/openaiCompatible/index.js"); return new OpenAICompatibleProvider(modelName, sdk, undefined, openaiCompatCreds); }, process.env.OPENAI_COMPATIBLE_MODEL || undefined, // Enable auto-discovery when no model specified ["openai-compatible", "vllm", "compatible"], PROVIDER_DESCRIPTORS_BY_NAME.get(AIProviderName.OPENAI_COMPATIBLE)); // Register OpenRouter provider (300+ models from 60+ providers) ProviderFactory.registerProvider(AIProviderName.OPENROUTER, async (modelName, _providerName, sdk, _region, credentials) => { const openrouterCreds = credentials; const { OpenRouterProvider } = await import("../providers/openRouter/index.js"); return new OpenRouterProvider(modelName, sdk, undefined, openrouterCreds); }, // OpenRouter retired `anthropic/claude-3-5-sonnet` in late 2025 — see // src/lib/providers/openRouter.ts and src/lib/utils/modelChoices.ts // for the rationale. Keep the three defaults aligned at the new // model (claude-sonnet-4.5) so the registry doesn't override the // provider's getDefault. process.env.OPENROUTER_MODEL || OpenRouterModels.CLAUDE_SONNET_4_5, ["openrouter", "or"], PROVIDER_DESCRIPTORS_BY_NAME.get(AIProviderName.OPENROUTER)); // Register Amazon SageMaker provider ProviderFactory.registerProvider(AIProviderName.SAGEMAKER, async (modelName, _providerName, _sdk, region, credentials) => { const sagemakerCreds = credentials; const { AmazonSageMakerProvider } = await import("../providers/amazonSagemaker.js"); return new AmazonSageMakerProvider(modelName, undefined, region, undefined, sagemakerCreds); }, process.env.SAGEMAKER_MODEL || "sagemaker-model", ["sagemaker", "aws-sagemaker"], PROVIDER_DESCRIPTORS_BY_NAME.get(AIProviderName.SAGEMAKER)); // DeepSeek migrated to the JSON catalog (catalog/deepseek.json) — it is // now registered by the "config-driven OpenAI-compatible catalog // providers" loop above, not a dedicated block here. // Register NVIDIA NIM provider ProviderFactory.registerProvider(AIProviderName.NVIDIA_NIM, async (modelName, _providerName, sdk, _region, credentials) => { const nimCreds = credentials; const { NvidiaNimProvider } = await import("../providers/nvidiaNim/index.js"); return new NvidiaNimProvider(modelName, sdk, undefined, nimCreds); }, process.env.NVIDIA_NIM_MODEL || PROVIDER_DESCRIPTORS_BY_NAME.get(AIProviderName.NVIDIA_NIM) ?.defaultModel || NvidiaNimModels.GPT_OSS_20B, ["nvidia", "nim", "nvidia-nim"], PROVIDER_DESCRIPTORS_BY_NAME.get(AIProviderName.NVIDIA_NIM)); // Register LM Studio provider (local) ProviderFactory.registerProvider(AIProviderName.LM_STUDIO, async (modelName, _providerName, sdk, _region, credentials) => { const lmStudioCreds = credentials; const { LMStudioProvider } = await import("../providers/lmStudio.js"); return new LMStudioProvider(modelName, sdk, undefined, lmStudioCreds); }, process.env.LM_STUDIO_MODEL || undefined, ["lmstudio", "lm-studio", "lms"], PROVIDER_DESCRIPTORS_BY_NAME.get(AIProviderName.LM_STUDIO)); // Register llama.cpp provider (local) ProviderFactory.registerProvider(AIProviderName.LLAMACPP, async (modelName, _providerName, sdk, _region, credentials) => { const llamaCppCreds = credentials; const { LlamaCppProvider } = await import("../providers/llamaCpp.js"); return new LlamaCppProvider(modelName, sdk, undefined, llamaCppCreds); }, process.env.LLAMACPP_MODEL || undefined, ["llamacpp", "llama.cpp", "llama-cpp"], PROVIDER_DESCRIPTORS_BY_NAME.get(AIProviderName.LLAMACPP)); // Register Cohere provider ProviderFactory.registerProvider(AIProviderName.COHERE, async (modelName, _providerName, sdk, _region, credentials) => { const cohereCreds = credentials; const { CohereProvider } = await import("../providers/cohere.js"); return new CohereProvider(modelName, sdk, undefined, cohereCreds); }, process.env.COHERE_MODEL || CohereModels.COMMAND_R_PLUS, ["cohere"], PROVIDER_DESCRIPTORS_BY_NAME.get(AIProviderName.COHERE)); // Register Voyage AI embeddings provider ProviderFactory.registerProvider(AIProviderName.VOYAGE, async (modelName, _providerName, sdk, _region, credentials) => { const voyageCreds = credentials; const { VoyageProvider } = await import("../providers/voyage.js"); return new VoyageProvider(modelName, sdk, undefined, voyageCreds); }, process.env.VOYAGE_MODEL || VoyageModels.VOYAGE_3_5, ["voyage", "voyage-ai"], PROVIDER_DESCRIPTORS_BY_NAME.get(AIProviderName.VOYAGE)); // Register Jina AI embeddings + reranking provider ProviderFactory.registerProvider(AIProviderName.JINA, async (modelName, _providerName, sdk, _region, credentials) => { const jinaCreds = credentials; const { JinaProvider } = await import("../providers/jina.js"); return new JinaProvider(modelName, sdk, undefined, jinaCreds); }, process.env.JINA_MODEL || JinaModels.JINA_EMBEDDINGS_V3, ["jina", "jina-ai"], PROVIDER_DESCRIPTORS_BY_NAME.get(AIProviderName.JINA)); // Register Stability AI image-gen provider ProviderFactory.registerProvider(AIProviderName.STABILITY, async (modelName, _providerName, sdk, _region, credentials) => { const stabilityCreds = credentials; const { StabilityProvider } = await import("../providers/stability.js"); return new StabilityProvider(modelName, sdk, undefined, stabilityCreds); }, process.env.STABILITY_MODEL || StabilityModels.STABLE_IMAGE_ULTRA, ["stability", "stability-ai", "sd"], PROVIDER_DESCRIPTORS_BY_NAME.get(AIProviderName.STABILITY)); // Register Ideogram image-gen provider ProviderFactory.registerProvider(AIProviderName.IDEOGRAM, async (modelName, _providerName, sdk, _region, credentials) => { const ideogramCreds = credentials; const { IdeogramProvider } = await import("../providers/ideogram.js"); return new IdeogramProvider(modelName, sdk, undefined, ideogramCreds); }, process.env.IDEOGRAM_MODEL || IdeogramModels.IDEOGRAM_V3, ["ideogram"], PROVIDER_DESCRIPTORS_BY_NAME.get(AIProviderName.IDEOGRAM)); // Register Replicate LLM provider (multi-modal — also serves video / // avatar / music handlers via dedicated processors registered below) ProviderFactory.registerProvider(AIProviderName.REPLICATE, async (modelName, _providerName, sdk, _region, credentials) => { const replicateCreds = credentials; const { ReplicateProvider } = await import("../providers/replicate.js"); return new ReplicateProvider(modelName, sdk, undefined, replicateCreds); }, process.env.REPLICATE_MODEL || ReplicateModels.LLAMA_3_70B_INSTRUCT, ["replicate"], PROVIDER_DESCRIPTORS_BY_NAME.get(AIProviderName.REPLICATE)); // Register Recraft image-gen provider ProviderFactory.registerProvider(AIProviderName.RECRAFT, async (modelName, _providerName, sdk, _region, credentials) => { const recraftCreds = credentials; const { RecraftProvider } = await import("../providers/recraft.js"); return new RecraftProvider(modelName, sdk, undefined, recraftCreds); }, process.env.RECRAFT_MODEL || RecraftModels.RECRAFT_V3, ["recraft"], PROVIDER_DESCRIPTORS_BY_NAME.get(AIProviderName.RECRAFT)); // Register TypeSafe (Jev) — the `decide` inference type, not text. // Its descriptor declares inferenceKinds: ["decide"], so nothing in the // generation fallback chain can reach it. ProviderFactory.registerProvider(AIProviderName.TYPESAFE, async (modelName, _providerName, sdk, _region, credentials) => { const typesafeCreds = credentials; const { TypeSafeProvider } = await import("../providers/typesafe.js"); return new TypeSafeProvider(modelName, sdk, undefined, typesafeCreds); }, process.env.TYPESAFE_MODEL || TypeSafeModels.JEV_LATEST, ["jev", "typesafe-ai"], PROVIDER_DESCRIPTORS_BY_NAME.get(AIProviderName.TYPESAFE)); // Register Laya — the second `decide` provider, reached at whatever base // URL its config names. Its descriptor declares inferenceKinds: // ["decide"], so nothing in the generation fallback chain can reach it. ProviderFactory.registerProvider(AIProviderName.LAYA, async (modelName, _providerName, sdk, _region, credentials) => { const layaCreds = credentials; const { LayaProvider } = await import("../providers/laya.js"); return new LayaProvider(modelName, sdk, undefined, layaCreds); }, process.env.LAYA_MODEL || LayaModels.TYPED_DECISIONS, [], PROVIDER_DESCRIPTORS_BY_NAME.get(AIProviderName.LAYA)); // Register XOR — the third `decide` provider, reached at whatever base // URL its config names. Its descriptor declares inferenceKinds: // ["decide"], so nothing in the generation fallback chain can reach it. ProviderFactory.registerProvider(AIProviderName.XOR, async (modelName, _providerName, sdk, _region, credentials) => { const xorCreds = credentials; const { XorProvider } = await import("../providers/xor.js"); return new XorProvider(modelName, sdk, undefined, xorCreds); }, process.env.XOR_MODEL || XorModels.XOR_1_1, [], PROVIDER_DESCRIPTORS_BY_NAME.get(AIProviderName.XOR)); // Register the Perplexity Decisions API — a `decide` provider reached at // Perplexity's public endpoint. Its descriptor declares inferenceKinds: // ["decide"], so nothing in the generation fallback chain can reach it, // and it is registered apart from the `perplexity` text provider. ProviderFactory.registerProvider(AIProviderName.PERPLEXITY_DECIDER, async (modelName, _providerName, sdk, _region, credentials) => { const perplexityDeciderCreds = credentials; const { PerplexityDeciderProvider } = await import("../providers/perplexityDecider.js"); return new PerplexityDeciderProvider(modelName, sdk, undefined, perplexityDeciderCreds); }, process.env.PERPLEXITY_DECIDER_MODEL || PerplexityDeciderModels.PPLX_DECIDER_V1_27B, [], PROVIDER_DESCRIPTORS_BY_NAME.get(AIProviderName.PERPLEXITY_DECIDER)); // Register Cloudflare Clef — a `decide` provider reached through the // Workers AI REST API. Its descriptor declares inferenceKinds: // ["decide"], so nothing in the generation fallback chain can reach it, // and it is registered apart from the `cloudflare` text provider. ProviderFactory.registerProvider(AIProviderName.CLOUDFLARE_CLEF, async (modelName, _providerName, sdk, _region, credentials) => { const cloudflareClefCreds = credentials; const { CloudflareClefProvider } = await import("../providers/cloudflareClef.js"); return new CloudflareClefProvider(modelName, sdk, undefined, cloudflareClefCreds); }, process.env.CLOUDFLARE_CLEF_MODEL || CloudflareClefModels.CLEF, [], PROVIDER_DESCRIPTORS_BY_NAME.get(AIProviderName.CLOUDFLARE_CLEF)); logger.debug("All AI providers registered successfully"); // ===== MEDIA HANDLER REGISTRATION ===== // Single registration path (Task 11): each ecosystem barrel (voice, // adapters/video, avatar, music) owns its own provider-name catalog // wiring (MEDIA_HANDLER_CATALOG, Task 8) and exposes an idempotent // registerDefault*Handlers() function (Task 10) that constructs and // registers every shipped handler whose backing credentials are // present in process.env. This block's only job is to invoke each of // the six functions via dynamic import — no hand-rolled // `new XHandler()` + `registerHandler()` calls live here anymore. // Each ecosystem keeps its own try/catch so one broken import can't // take down the other five (matches the previous block's isolation). // ===== TTS HANDLER REGISTRATION ===== try { const { registerDefaultTTSHandlers } = await import("../voice/index.js"); registerDefaultTTSHandlers(); logger.debug("TTS handler registration attempted", { providers: providerChoicesFor("tts"), }); } catch (ttsError) { logger.warn("Failed to register TTS handlers - TTS functionality will be unavailable", { error: ttsError instanceof Error ? ttsError.message : String(ttsError), }); // Don't throw - TTS is optional functionality } // ===== STT HANDLER REGISTRATION ===== try { const { registerDefaultSTTHandlers } = await import("../voice/index.js"); registerDefaultSTTHandlers(); logger.debug("STT handler registration attempted", { providers: providerChoicesFor("stt"), }); } catch (sttError) { logger.warn("Failed to register STT handlers - STT functionality will be unavailable", { error: sttError instanceof Error ? sttError.message : String(sttError), }); } // ===== REALTIME HANDLER REGISTRATION ===== try { const { registerDefaultRealtimeHandlers, RealtimeProcessor } = await import("../voice/index.js"); registerDefaultRealtimeHandlers(); // M9 + NEW4: registerDefaultRealtimeHandlers() swallows per-handler // construction failures internally (it's a shared, idempotent // barrel function used by every entry point — see voice/index.ts), // so the exact per-handler exception text the old inline block // captured is no longer available here. Recover per-name pass/fail // via supports() instead so getRegistrationReport() stays truthful // for callers that poll it. const realtimeNames = providerChoicesFor("realtime"); const realtimeOutcomes = {}; for (const name of realtimeNames) { realtimeOutcomes[name] = RealtimeProcessor.supports(name) ? "ok" : "registration failed or handler unavailable"; } // NEW4: report the actual per-handler outcomes instead of an // unconditional success log. Stored on the registry so callers can // introspect via getRegistrationReport(). ProviderRegistry.realtimeRegistration = realtimeOutcomes; const skipped = Object.entries(realtimeOutcomes).filter(([, v]) => v !== "ok"); if (skipped.length === 0) { logger.info(`[ProviderRegistry] Realtime handlers registered: ${realtimeNames.join(", ")}`); } else { logger.warn(`[ProviderRegistry] Realtime handlers partial: ${skipped.length} skipped`, { outcomes: realtimeOutcomes }); } } catch (realtimeError) { logger.warn("Failed to register Realtime handlers - Realtime functionality will be unavailable", { error: realtimeError instanceof Error ? realtimeError.message : String(realtimeError), }); } // ===== VIDEO HANDLER REGISTRATION ===== try { const { registerDefaultVideoHandlers } = await import("../adapters/video/index.js"); registerDefaultVideoHandlers(); logger.debug("Video handler registration attempted", { providers: providerChoicesFor("video"), }); } catch (err) { logger.warn(`[ProviderRegistry] video registration block failed: ${err instanceof Error ? err.message : String(err)}`); } // ===== AVATAR HANDLER REGISTRATION ===== try { const { registerDefaultAvatarHandlers } = await import("../avatar/index.js"); registerDefaultAvatarHandlers(); logger.debug("Avatar handler registration attempted", { providers: providerChoicesFor("avatar"), }); } catch (avatarError) { logger.warn("Failed to register Avatar handlers - Avatar functionality will be unavailable", { error: avatarError instanceof Error ? avatarError.message : String(avatarError), }); } // ===== MUSIC HANDLER REGISTRATION ===== try { const { registerDefaultMusicHandlers } = await import("../music/index.js"); registerDefaultMusicHandlers(); logger.debug("Music handler registration attempted", { providers: providerChoicesFor("music"), }); } catch (musicError) { logger.warn("Failed to register Music handlers - Music functionality will be unavailable", { error: musicError instanceof Error ? musicError.message : String(musicError), }); } // Mark registered ONLY after all blocks (AI + voice) attempted, so a // subsequent registerAllProviders() call does not short-circuit when an // optional handler block silently failed. this.registered = true; } catch (error) { logger.error("Failed to register providers:", error); throw error; } } /** * Check if providers are registered */ static isRegistered() { return this.registered; } /** * Clear registrations (for testing) */ static clearRegistrations() { ProviderFactory.clearRegistrations(); this.registered = false; this.registrationPromise = null; // Reset realtime registration too — otherwise getRegistrationReport() // can surface stale data from a previous run if the realtime block // failed before reaching `realtimeRegistration = realtimeOutcomes`. ProviderRegistry.realtimeRegistration = {}; } /** * Set registry options (should be called before initialization) */ static setOptions(options) { this.options = { ...this.options, ...options }; logger.debug("Provider registry options updated:", this.options); } /** * Get current registry options */ static getOptions() { return { ...this.options }; } } // Note: Providers are registered explicitly when needed to avoid circular dependencies