@revealui/ai 0.1.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.
- package/LICENSE +22 -0
- package/LICENSE.commercial +112 -0
- package/README.md +314 -0
- package/dist/a2a/card.d.ts +26 -0
- package/dist/a2a/card.d.ts.map +1 -0
- package/dist/a2a/card.js +173 -0
- package/dist/a2a/handler.d.ts +26 -0
- package/dist/a2a/handler.d.ts.map +1 -0
- package/dist/a2a/handler.js +170 -0
- package/dist/a2a/index.d.ts +10 -0
- package/dist/a2a/index.d.ts.map +1 -0
- package/dist/a2a/index.js +9 -0
- package/dist/a2a/task-store.d.ts +42 -0
- package/dist/a2a/task-store.d.ts.map +1 -0
- package/dist/a2a/task-store.js +99 -0
- package/dist/audit/emitter.d.ts +34 -0
- package/dist/audit/emitter.d.ts.map +1 -0
- package/dist/audit/emitter.js +34 -0
- package/dist/audit/index.d.ts +44 -0
- package/dist/audit/index.d.ts.map +1 -0
- package/dist/audit/index.js +48 -0
- package/dist/audit/observer.d.ts +108 -0
- package/dist/audit/observer.d.ts.map +1 -0
- package/dist/audit/observer.js +271 -0
- package/dist/audit/policy.d.ts +70 -0
- package/dist/audit/policy.d.ts.map +1 -0
- package/dist/audit/policy.js +209 -0
- package/dist/audit/store.d.ts +42 -0
- package/dist/audit/store.d.ts.map +1 -0
- package/dist/audit/store.js +80 -0
- package/dist/audit/types.d.ts +169 -0
- package/dist/audit/types.d.ts.map +1 -0
- package/dist/audit/types.js +80 -0
- package/dist/client/hooks/index.d.ts +22 -0
- package/dist/client/hooks/index.d.ts.map +1 -0
- package/dist/client/hooks/index.js +21 -0
- package/dist/client/hooks/useAgentContext.d.ts +30 -0
- package/dist/client/hooks/useAgentContext.d.ts.map +1 -0
- package/dist/client/hooks/useAgentContext.js +161 -0
- package/dist/client/hooks/useAgentEvents.d.ts +126 -0
- package/dist/client/hooks/useAgentEvents.d.ts.map +1 -0
- package/dist/client/hooks/useAgentEvents.js +232 -0
- package/dist/client/hooks/useAgentStream.d.ts +44 -0
- package/dist/client/hooks/useAgentStream.d.ts.map +1 -0
- package/dist/client/hooks/useAgentStream.js +101 -0
- package/dist/client/hooks/useEpisodicMemory.d.ts +25 -0
- package/dist/client/hooks/useEpisodicMemory.d.ts.map +1 -0
- package/dist/client/hooks/useEpisodicMemory.js +174 -0
- package/dist/client/hooks/useWorkingMemory.d.ts +57 -0
- package/dist/client/hooks/useWorkingMemory.d.ts.map +1 -0
- package/dist/client/hooks/useWorkingMemory.js +276 -0
- package/dist/client/index.d.ts +14 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +13 -0
- package/dist/embeddings/index.d.ts +51 -0
- package/dist/embeddings/index.d.ts.map +1 -0
- package/dist/embeddings/index.js +73 -0
- package/dist/index.d.ts +83 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +103 -0
- package/dist/inference/context-assembly.d.ts +27 -0
- package/dist/inference/context-assembly.d.ts.map +1 -0
- package/dist/inference/context-assembly.js +81 -0
- package/dist/inference/overflow-compressor.d.ts +17 -0
- package/dist/inference/overflow-compressor.d.ts.map +1 -0
- package/dist/inference/overflow-compressor.js +40 -0
- package/dist/inference/runRag.d.ts +35 -0
- package/dist/inference/runRag.d.ts.map +1 -0
- package/dist/inference/runRag.js +53 -0
- package/dist/ingestion/bm25.d.ts +29 -0
- package/dist/ingestion/bm25.d.ts.map +1 -0
- package/dist/ingestion/bm25.js +161 -0
- package/dist/ingestion/cms-indexer.d.ts +39 -0
- package/dist/ingestion/cms-indexer.d.ts.map +1 -0
- package/dist/ingestion/cms-indexer.js +74 -0
- package/dist/ingestion/file-parsers.d.ts +51 -0
- package/dist/ingestion/file-parsers.d.ts.map +1 -0
- package/dist/ingestion/file-parsers.js +247 -0
- package/dist/ingestion/hybrid-search.d.ts +22 -0
- package/dist/ingestion/hybrid-search.d.ts.map +1 -0
- package/dist/ingestion/hybrid-search.js +63 -0
- package/dist/ingestion/index.d.ts +9 -0
- package/dist/ingestion/index.d.ts.map +1 -0
- package/dist/ingestion/index.js +8 -0
- package/dist/ingestion/pipeline.d.ts +35 -0
- package/dist/ingestion/pipeline.d.ts.map +1 -0
- package/dist/ingestion/pipeline.js +114 -0
- package/dist/ingestion/rag-vector-service.d.ts +34 -0
- package/dist/ingestion/rag-vector-service.d.ts.map +1 -0
- package/dist/ingestion/rag-vector-service.js +98 -0
- package/dist/ingestion/reranker.d.ts +10 -0
- package/dist/ingestion/reranker.d.ts.map +1 -0
- package/dist/ingestion/reranker.js +41 -0
- package/dist/ingestion/text-splitter.d.ts +25 -0
- package/dist/ingestion/text-splitter.d.ts.map +1 -0
- package/dist/ingestion/text-splitter.js +119 -0
- package/dist/llm/cache-utils.d.ts +146 -0
- package/dist/llm/cache-utils.d.ts.map +1 -0
- package/dist/llm/cache-utils.js +204 -0
- package/dist/llm/client.d.ts +134 -0
- package/dist/llm/client.d.ts.map +1 -0
- package/dist/llm/client.js +497 -0
- package/dist/llm/key-validator.d.ts +25 -0
- package/dist/llm/key-validator.d.ts.map +1 -0
- package/dist/llm/key-validator.js +101 -0
- package/dist/llm/provider-health.d.ts +40 -0
- package/dist/llm/provider-health.d.ts.map +1 -0
- package/dist/llm/provider-health.js +97 -0
- package/dist/llm/providers/anthropic.d.ts +31 -0
- package/dist/llm/providers/anthropic.d.ts.map +1 -0
- package/dist/llm/providers/anthropic.js +248 -0
- package/dist/llm/providers/base.d.ts +111 -0
- package/dist/llm/providers/base.d.ts.map +1 -0
- package/dist/llm/providers/base.js +6 -0
- package/dist/llm/providers/groq.d.ts +23 -0
- package/dist/llm/providers/groq.d.ts.map +1 -0
- package/dist/llm/providers/groq.js +27 -0
- package/dist/llm/providers/ollama.d.ts +27 -0
- package/dist/llm/providers/ollama.d.ts.map +1 -0
- package/dist/llm/providers/ollama.js +48 -0
- package/dist/llm/providers/openai.d.ts +19 -0
- package/dist/llm/providers/openai.d.ts.map +1 -0
- package/dist/llm/providers/openai.js +245 -0
- package/dist/llm/providers/vultr.d.ts +18 -0
- package/dist/llm/providers/vultr.d.ts.map +1 -0
- package/dist/llm/providers/vultr.js +168 -0
- package/dist/llm/response-cache.d.ts +166 -0
- package/dist/llm/response-cache.d.ts.map +1 -0
- package/dist/llm/response-cache.js +233 -0
- package/dist/llm/semantic-cache.d.ts +179 -0
- package/dist/llm/semantic-cache.d.ts.map +1 -0
- package/dist/llm/semantic-cache.js +306 -0
- package/dist/llm/server.d.ts +14 -0
- package/dist/llm/server.d.ts.map +1 -0
- package/dist/llm/server.js +15 -0
- package/dist/llm/token-counter.d.ts +48 -0
- package/dist/llm/token-counter.d.ts.map +1 -0
- package/dist/llm/token-counter.js +77 -0
- package/dist/llm/workspace-provider-config.d.ts +38 -0
- package/dist/llm/workspace-provider-config.d.ts.map +1 -0
- package/dist/llm/workspace-provider-config.js +47 -0
- package/dist/memory/agent/context-manager.d.ts +148 -0
- package/dist/memory/agent/context-manager.d.ts.map +1 -0
- package/dist/memory/agent/context-manager.js +284 -0
- package/dist/memory/agent/index.d.ts +7 -0
- package/dist/memory/agent/index.d.ts.map +1 -0
- package/dist/memory/agent/index.js +6 -0
- package/dist/memory/crdt/index.d.ts +13 -0
- package/dist/memory/crdt/index.d.ts.map +1 -0
- package/dist/memory/crdt/index.js +12 -0
- package/dist/memory/crdt/lww-register.d.ts +108 -0
- package/dist/memory/crdt/lww-register.d.ts.map +1 -0
- package/dist/memory/crdt/lww-register.js +169 -0
- package/dist/memory/crdt/or-set.d.ts +141 -0
- package/dist/memory/crdt/or-set.d.ts.map +1 -0
- package/dist/memory/crdt/or-set.js +291 -0
- package/dist/memory/crdt/pn-counter.d.ts +116 -0
- package/dist/memory/crdt/pn-counter.d.ts.map +1 -0
- package/dist/memory/crdt/pn-counter.js +174 -0
- package/dist/memory/crdt/vector-clock.d.ts +115 -0
- package/dist/memory/crdt/vector-clock.d.ts.map +1 -0
- package/dist/memory/crdt/vector-clock.js +179 -0
- package/dist/memory/errors/index.d.ts +56 -0
- package/dist/memory/errors/index.d.ts.map +1 -0
- package/dist/memory/errors/index.js +85 -0
- package/dist/memory/index.d.ts +21 -0
- package/dist/memory/index.d.ts.map +1 -0
- package/dist/memory/index.js +20 -0
- package/dist/memory/persistence/crdt-persistence.d.ts +85 -0
- package/dist/memory/persistence/crdt-persistence.d.ts.map +1 -0
- package/dist/memory/persistence/crdt-persistence.js +204 -0
- package/dist/memory/persistence/index.d.ts +7 -0
- package/dist/memory/persistence/index.d.ts.map +1 -0
- package/dist/memory/persistence/index.js +6 -0
- package/dist/memory/preferences/index.d.ts +7 -0
- package/dist/memory/preferences/index.d.ts.map +1 -0
- package/dist/memory/preferences/index.js +6 -0
- package/dist/memory/preferences/user-preferences-manager.d.ts +133 -0
- package/dist/memory/preferences/user-preferences-manager.d.ts.map +1 -0
- package/dist/memory/preferences/user-preferences-manager.js +342 -0
- package/dist/memory/services/index.d.ts +8 -0
- package/dist/memory/services/index.d.ts.map +1 -0
- package/dist/memory/services/index.js +6 -0
- package/dist/memory/services/node-id-service.d.ts +75 -0
- package/dist/memory/services/node-id-service.d.ts.map +1 -0
- package/dist/memory/services/node-id-service.js +190 -0
- package/dist/memory/stores/episodic-memory.d.ts +182 -0
- package/dist/memory/stores/episodic-memory.d.ts.map +1 -0
- package/dist/memory/stores/episodic-memory.js +378 -0
- package/dist/memory/stores/index.d.ts +16 -0
- package/dist/memory/stores/index.d.ts.map +1 -0
- package/dist/memory/stores/index.js +15 -0
- package/dist/memory/stores/procedural-memory.d.ts +89 -0
- package/dist/memory/stores/procedural-memory.d.ts.map +1 -0
- package/dist/memory/stores/procedural-memory.js +152 -0
- package/dist/memory/stores/semantic-memory.d.ts +92 -0
- package/dist/memory/stores/semantic-memory.d.ts.map +1 -0
- package/dist/memory/stores/semantic-memory.js +155 -0
- package/dist/memory/stores/working-memory.d.ts +225 -0
- package/dist/memory/stores/working-memory.d.ts.map +1 -0
- package/dist/memory/stores/working-memory.js +336 -0
- package/dist/memory/utils/deep-clone.d.ts +10 -0
- package/dist/memory/utils/deep-clone.d.ts.map +1 -0
- package/dist/memory/utils/deep-clone.js +9 -0
- package/dist/memory/utils/index.d.ts +8 -0
- package/dist/memory/utils/index.d.ts.map +1 -0
- package/dist/memory/utils/index.js +7 -0
- package/dist/memory/utils/logger.d.ts +21 -0
- package/dist/memory/utils/logger.d.ts.map +1 -0
- package/dist/memory/utils/logger.js +62 -0
- package/dist/memory/utils/sql-helpers.d.ts +97 -0
- package/dist/memory/utils/sql-helpers.d.ts.map +1 -0
- package/dist/memory/utils/sql-helpers.js +214 -0
- package/dist/memory/utils/validation.d.ts +62 -0
- package/dist/memory/utils/validation.d.ts.map +1 -0
- package/dist/memory/utils/validation.js +244 -0
- package/dist/memory/vector/index.d.ts +12 -0
- package/dist/memory/vector/index.d.ts.map +1 -0
- package/dist/memory/vector/index.js +14 -0
- package/dist/memory/vector/vector-memory-service.d.ts +88 -0
- package/dist/memory/vector/vector-memory-service.d.ts.map +1 -0
- package/dist/memory/vector/vector-memory-service.js +335 -0
- package/dist/observability/logger.d.ts +79 -0
- package/dist/observability/logger.d.ts.map +1 -0
- package/dist/observability/logger.js +165 -0
- package/dist/observability/metrics.d.ts +43 -0
- package/dist/observability/metrics.d.ts.map +1 -0
- package/dist/observability/metrics.js +197 -0
- package/dist/observability/query.d.ts +150 -0
- package/dist/observability/query.d.ts.map +1 -0
- package/dist/observability/query.js +339 -0
- package/dist/observability/types.d.ts +140 -0
- package/dist/observability/types.d.ts.map +1 -0
- package/dist/observability/types.js +6 -0
- package/dist/orchestration/agent.d.ts +98 -0
- package/dist/orchestration/agent.d.ts.map +1 -0
- package/dist/orchestration/agent.js +6 -0
- package/dist/orchestration/defaults.d.ts +21 -0
- package/dist/orchestration/defaults.d.ts.map +1 -0
- package/dist/orchestration/defaults.js +22 -0
- package/dist/orchestration/memory-integration.d.ts +58 -0
- package/dist/orchestration/memory-integration.d.ts.map +1 -0
- package/dist/orchestration/memory-integration.js +130 -0
- package/dist/orchestration/orchestrator.d.ts +67 -0
- package/dist/orchestration/orchestrator.d.ts.map +1 -0
- package/dist/orchestration/orchestrator.js +174 -0
- package/dist/orchestration/runtime.d.ts +82 -0
- package/dist/orchestration/runtime.d.ts.map +1 -0
- package/dist/orchestration/runtime.js +251 -0
- package/dist/orchestration/streaming-runtime.d.ts +36 -0
- package/dist/orchestration/streaming-runtime.d.ts.map +1 -0
- package/dist/orchestration/streaming-runtime.js +175 -0
- package/dist/orchestration/ticket-agent.d.ts +70 -0
- package/dist/orchestration/ticket-agent.d.ts.map +1 -0
- package/dist/orchestration/ticket-agent.js +146 -0
- package/dist/skills/activation/index.d.ts +7 -0
- package/dist/skills/activation/index.d.ts.map +1 -0
- package/dist/skills/activation/index.js +6 -0
- package/dist/skills/activation/skill-activator.d.ts +68 -0
- package/dist/skills/activation/skill-activator.d.ts.map +1 -0
- package/dist/skills/activation/skill-activator.js +224 -0
- package/dist/skills/catalog/catalog-search.d.ts +55 -0
- package/dist/skills/catalog/catalog-search.d.ts.map +1 -0
- package/dist/skills/catalog/catalog-search.js +111 -0
- package/dist/skills/catalog/catalog-types.d.ts +81 -0
- package/dist/skills/catalog/catalog-types.d.ts.map +1 -0
- package/dist/skills/catalog/catalog-types.js +66 -0
- package/dist/skills/catalog/index.d.ts +9 -0
- package/dist/skills/catalog/index.d.ts.map +1 -0
- package/dist/skills/catalog/index.js +7 -0
- package/dist/skills/catalog/vercel-catalog.d.ts +42 -0
- package/dist/skills/catalog/vercel-catalog.d.ts.map +1 -0
- package/dist/skills/catalog/vercel-catalog.js +189 -0
- package/dist/skills/compat/index.d.ts +9 -0
- package/dist/skills/compat/index.d.ts.map +1 -0
- package/dist/skills/compat/index.js +8 -0
- package/dist/skills/compat/skill-enhancer.d.ts +37 -0
- package/dist/skills/compat/skill-enhancer.d.ts.map +1 -0
- package/dist/skills/compat/skill-enhancer.js +76 -0
- package/dist/skills/compat/tool-mapper.d.ts +61 -0
- package/dist/skills/compat/tool-mapper.d.ts.map +1 -0
- package/dist/skills/compat/tool-mapper.js +168 -0
- package/dist/skills/compat/vercel-compat.d.ts +33 -0
- package/dist/skills/compat/vercel-compat.d.ts.map +1 -0
- package/dist/skills/compat/vercel-compat.js +132 -0
- package/dist/skills/index.d.ts +40 -0
- package/dist/skills/index.d.ts.map +1 -0
- package/dist/skills/index.js +47 -0
- package/dist/skills/integration/agent-skill-provider.d.ts +94 -0
- package/dist/skills/integration/agent-skill-provider.d.ts.map +1 -0
- package/dist/skills/integration/agent-skill-provider.js +161 -0
- package/dist/skills/integration/index.d.ts +7 -0
- package/dist/skills/integration/index.d.ts.map +1 -0
- package/dist/skills/integration/index.js +6 -0
- package/dist/skills/loader/github-loader.d.ts +61 -0
- package/dist/skills/loader/github-loader.d.ts.map +1 -0
- package/dist/skills/loader/github-loader.js +176 -0
- package/dist/skills/loader/index.d.ts +10 -0
- package/dist/skills/loader/index.d.ts.map +1 -0
- package/dist/skills/loader/index.js +9 -0
- package/dist/skills/loader/local-loader.d.ts +56 -0
- package/dist/skills/loader/local-loader.d.ts.map +1 -0
- package/dist/skills/loader/local-loader.js +186 -0
- package/dist/skills/loader/vercel-loader.d.ts +64 -0
- package/dist/skills/loader/vercel-loader.d.ts.map +1 -0
- package/dist/skills/loader/vercel-loader.js +313 -0
- package/dist/skills/loader/vercel-types.d.ts +64 -0
- package/dist/skills/loader/vercel-types.d.ts.map +1 -0
- package/dist/skills/loader/vercel-types.js +55 -0
- package/dist/skills/parser/index.d.ts +7 -0
- package/dist/skills/parser/index.d.ts.map +1 -0
- package/dist/skills/parser/index.js +6 -0
- package/dist/skills/parser/skill-md-parser.d.ts +64 -0
- package/dist/skills/parser/skill-md-parser.d.ts.map +1 -0
- package/dist/skills/parser/skill-md-parser.js +242 -0
- package/dist/skills/registry/index.d.ts +7 -0
- package/dist/skills/registry/index.d.ts.map +1 -0
- package/dist/skills/registry/index.js +6 -0
- package/dist/skills/registry/skill-registry.d.ts +133 -0
- package/dist/skills/registry/skill-registry.d.ts.map +1 -0
- package/dist/skills/registry/skill-registry.js +373 -0
- package/dist/skills/types.d.ts +216 -0
- package/dist/skills/types.d.ts.map +1 -0
- package/dist/skills/types.js +176 -0
- package/dist/templates/agent-spec.d.ts +138 -0
- package/dist/templates/agent-spec.d.ts.map +1 -0
- package/dist/templates/agent-spec.js +138 -0
- package/dist/templates/index.d.ts +56 -0
- package/dist/templates/index.d.ts.map +1 -0
- package/dist/templates/index.js +58 -0
- package/dist/templates/prompt-spec.d.ts +140 -0
- package/dist/templates/prompt-spec.d.ts.map +1 -0
- package/dist/templates/prompt-spec.js +210 -0
- package/dist/templates/skill-spec.d.ts +106 -0
- package/dist/templates/skill-spec.d.ts.map +1 -0
- package/dist/templates/skill-spec.js +119 -0
- package/dist/tools/base.d.ts +74 -0
- package/dist/tools/base.d.ts.map +1 -0
- package/dist/tools/base.js +6 -0
- package/dist/tools/cms/collection-tools.d.ts +36 -0
- package/dist/tools/cms/collection-tools.d.ts.map +1 -0
- package/dist/tools/cms/collection-tools.js +178 -0
- package/dist/tools/cms/factory.d.ts +89 -0
- package/dist/tools/cms/factory.d.ts.map +1 -0
- package/dist/tools/cms/factory.js +462 -0
- package/dist/tools/cms/global-tools.d.ts +21 -0
- package/dist/tools/cms/global-tools.d.ts.map +1 -0
- package/dist/tools/cms/global-tools.js +92 -0
- package/dist/tools/cms/index.d.ts +11 -0
- package/dist/tools/cms/index.d.ts.map +1 -0
- package/dist/tools/cms/index.js +11 -0
- package/dist/tools/cms/media-tools.d.ts +31 -0
- package/dist/tools/cms/media-tools.d.ts.map +1 -0
- package/dist/tools/cms/media-tools.js +140 -0
- package/dist/tools/cms/user-tools.d.ts +31 -0
- package/dist/tools/cms/user-tools.d.ts.map +1 -0
- package/dist/tools/cms/user-tools.js +135 -0
- package/dist/tools/deduplicator.d.ts +19 -0
- package/dist/tools/deduplicator.d.ts.map +1 -0
- package/dist/tools/deduplicator.js +53 -0
- package/dist/tools/document-summarizer.d.ts +11 -0
- package/dist/tools/document-summarizer.d.ts.map +1 -0
- package/dist/tools/document-summarizer.js +82 -0
- package/dist/tools/mcp-adapter.d.ts +66 -0
- package/dist/tools/mcp-adapter.d.ts.map +1 -0
- package/dist/tools/mcp-adapter.js +152 -0
- package/dist/tools/memory/index.d.ts +3 -0
- package/dist/tools/memory/index.d.ts.map +1 -0
- package/dist/tools/memory/index.js +1 -0
- package/dist/tools/memory/store-memory.d.ts +39 -0
- package/dist/tools/memory/store-memory.d.ts.map +1 -0
- package/dist/tools/memory/store-memory.js +94 -0
- package/dist/tools/registry.d.ts +14 -0
- package/dist/tools/registry.d.ts.map +1 -0
- package/dist/tools/registry.js +48 -0
- package/dist/tools/ticket-tools.d.ts +31 -0
- package/dist/tools/ticket-tools.d.ts.map +1 -0
- package/dist/tools/ticket-tools.js +74 -0
- package/dist/tools/web/duck-duck-go.d.ts +52 -0
- package/dist/tools/web/duck-duck-go.d.ts.map +1 -0
- package/dist/tools/web/duck-duck-go.js +202 -0
- package/dist/tools/web/exa.d.ts +34 -0
- package/dist/tools/web/exa.d.ts.map +1 -0
- package/dist/tools/web/exa.js +80 -0
- package/dist/tools/web/index.d.ts +6 -0
- package/dist/tools/web/index.d.ts.map +1 -0
- package/dist/tools/web/index.js +4 -0
- package/dist/tools/web/scraper.d.ts +9 -0
- package/dist/tools/web/scraper.d.ts.map +1 -0
- package/dist/tools/web/scraper.js +118 -0
- package/dist/tools/web/tavily.d.ts +32 -0
- package/dist/tools/web/tavily.d.ts.map +1 -0
- package/dist/tools/web/tavily.js +73 -0
- package/dist/tools/web/types.d.ts +31 -0
- package/dist/tools/web/types.d.ts.map +1 -0
- package/dist/tools/web/types.js +9 -0
- package/package.json +143 -0
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified LLM Client
|
|
3
|
+
*
|
|
4
|
+
* Single interface for all LLM providers with fallback and rate limiting
|
|
5
|
+
*/
|
|
6
|
+
// =============================================================================
|
|
7
|
+
// Log redaction
|
|
8
|
+
// =============================================================================
|
|
9
|
+
const SENSITIVE_KEYS = new Set([
|
|
10
|
+
'apiKey',
|
|
11
|
+
'api_key',
|
|
12
|
+
'authorization',
|
|
13
|
+
'Authorization',
|
|
14
|
+
'x-ai-api-key',
|
|
15
|
+
'X-AI-Api-Key',
|
|
16
|
+
'token',
|
|
17
|
+
'secret',
|
|
18
|
+
'password',
|
|
19
|
+
'encryptedKey',
|
|
20
|
+
'encrypted_key',
|
|
21
|
+
]);
|
|
22
|
+
/**
|
|
23
|
+
* Redact sensitive fields before passing an object to a logger.
|
|
24
|
+
* Replaces API keys, tokens, and authorization headers with `[REDACTED]`.
|
|
25
|
+
* Recurses into nested plain objects; leaves arrays and primitives as-is.
|
|
26
|
+
*/
|
|
27
|
+
export function redactSensitiveFields(obj) {
|
|
28
|
+
const result = {};
|
|
29
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
30
|
+
if (SENSITIVE_KEYS.has(key)) {
|
|
31
|
+
result[key] = '[REDACTED]';
|
|
32
|
+
}
|
|
33
|
+
else if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
|
|
34
|
+
result[key] = redactSensitiveFields(value);
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
result[key] = value;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
import { decryptApiKey } from '@revealui/db/crypto';
|
|
43
|
+
import { tenantProviderConfigs, userApiKeys } from '@revealui/db/schema';
|
|
44
|
+
import { and, eq } from 'drizzle-orm';
|
|
45
|
+
import { AnthropicProvider } from './providers/anthropic.js';
|
|
46
|
+
import { GroqProvider } from './providers/groq.js';
|
|
47
|
+
import { OllamaProvider } from './providers/ollama.js';
|
|
48
|
+
import { OpenAIProvider } from './providers/openai.js';
|
|
49
|
+
import { VultrProvider } from './providers/vultr.js';
|
|
50
|
+
import { ResponseCache } from './response-cache.js';
|
|
51
|
+
import { SemanticCache, } from './semantic-cache.js';
|
|
52
|
+
import { estimateRequest as _estimateRequestTokens } from './token-counter.js';
|
|
53
|
+
export class LLMClient {
|
|
54
|
+
provider;
|
|
55
|
+
fallbackProvider;
|
|
56
|
+
config;
|
|
57
|
+
rateLimitState;
|
|
58
|
+
responseCache;
|
|
59
|
+
semanticCache;
|
|
60
|
+
healthMonitor;
|
|
61
|
+
/** Tracks the last resolved API key so we only recreate the provider when it changes */
|
|
62
|
+
currentApiKey;
|
|
63
|
+
constructor(config) {
|
|
64
|
+
this.config = config;
|
|
65
|
+
this.currentApiKey = config.apiKey;
|
|
66
|
+
this.rateLimitState = {
|
|
67
|
+
requests: [],
|
|
68
|
+
dailyRequests: 0,
|
|
69
|
+
lastReset: Date.now(),
|
|
70
|
+
};
|
|
71
|
+
// Initialize response cache if enabled
|
|
72
|
+
if (config.enableResponseCache) {
|
|
73
|
+
this.responseCache = new ResponseCache(config.responseCacheOptions);
|
|
74
|
+
}
|
|
75
|
+
// Initialize semantic cache if enabled
|
|
76
|
+
if (config.enableSemanticCache) {
|
|
77
|
+
this.semanticCache = new SemanticCache(config.semanticCacheOptions);
|
|
78
|
+
}
|
|
79
|
+
// Wire health monitor if provided
|
|
80
|
+
this.healthMonitor = config.healthMonitor;
|
|
81
|
+
// Create primary provider
|
|
82
|
+
this.provider = this.createProvider(config.provider, {
|
|
83
|
+
apiKey: config.apiKey,
|
|
84
|
+
baseURL: config.baseURL,
|
|
85
|
+
model: config.model,
|
|
86
|
+
temperature: config.temperature,
|
|
87
|
+
maxTokens: config.maxTokens,
|
|
88
|
+
});
|
|
89
|
+
// Create fallback provider if specified
|
|
90
|
+
if (config.fallbackProvider) {
|
|
91
|
+
this.fallbackProvider = this.createProvider(config.fallbackProvider, {
|
|
92
|
+
apiKey: config.apiKey, // Note: In practice, you'd want separate API keys
|
|
93
|
+
baseURL: config.baseURL,
|
|
94
|
+
model: config.model,
|
|
95
|
+
temperature: config.temperature,
|
|
96
|
+
maxTokens: config.maxTokens,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
createProvider(type, config) {
|
|
101
|
+
switch (type) {
|
|
102
|
+
case 'openai':
|
|
103
|
+
return new OpenAIProvider(config);
|
|
104
|
+
case 'anthropic':
|
|
105
|
+
return new AnthropicProvider({
|
|
106
|
+
...config,
|
|
107
|
+
enableCacheByDefault: this.config.enableCacheByDefault,
|
|
108
|
+
});
|
|
109
|
+
case 'vultr':
|
|
110
|
+
return new VultrProvider(config);
|
|
111
|
+
case 'groq':
|
|
112
|
+
return new GroqProvider(config);
|
|
113
|
+
case 'ollama':
|
|
114
|
+
return new OllamaProvider(config);
|
|
115
|
+
default:
|
|
116
|
+
throw new Error(`Unknown provider type: ${String(type)}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Re-resolve the API key via apiKeyFn (if configured) and recreate the provider
|
|
121
|
+
* when the key has changed. No-op if apiKeyFn is not set.
|
|
122
|
+
*/
|
|
123
|
+
async refreshProviderIfNeeded() {
|
|
124
|
+
if (!this.config.apiKeyFn)
|
|
125
|
+
return;
|
|
126
|
+
const newKey = await this.config.apiKeyFn();
|
|
127
|
+
if (newKey === this.currentApiKey)
|
|
128
|
+
return;
|
|
129
|
+
this.currentApiKey = newKey;
|
|
130
|
+
const providerConfig = {
|
|
131
|
+
apiKey: newKey,
|
|
132
|
+
baseURL: this.config.baseURL,
|
|
133
|
+
model: this.config.model,
|
|
134
|
+
temperature: this.config.temperature,
|
|
135
|
+
maxTokens: this.config.maxTokens,
|
|
136
|
+
};
|
|
137
|
+
this.provider = this.createProvider(this.config.provider, providerConfig);
|
|
138
|
+
if (this.config.fallbackProvider) {
|
|
139
|
+
this.fallbackProvider = this.createProvider(this.config.fallbackProvider, providerConfig);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
checkRateLimit() {
|
|
143
|
+
const now = Date.now();
|
|
144
|
+
const { rateLimit } = this.config;
|
|
145
|
+
if (!rateLimit) {
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
// Reset daily counter if needed
|
|
149
|
+
if (now - this.rateLimitState.lastReset > 24 * 60 * 60 * 1000) {
|
|
150
|
+
this.rateLimitState.dailyRequests = 0;
|
|
151
|
+
this.rateLimitState.lastReset = now;
|
|
152
|
+
}
|
|
153
|
+
// Check per-minute limit
|
|
154
|
+
if (rateLimit.requestsPerMinute) {
|
|
155
|
+
const oneMinuteAgo = now - 60 * 1000;
|
|
156
|
+
this.rateLimitState.requests = this.rateLimitState.requests.filter((time) => time > oneMinuteAgo);
|
|
157
|
+
if (this.rateLimitState.requests.length >= rateLimit.requestsPerMinute) {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
// Check daily limit
|
|
162
|
+
if (rateLimit.requestsPerDay) {
|
|
163
|
+
if (this.rateLimitState.dailyRequests >= rateLimit.requestsPerDay) {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
recordRequest() {
|
|
170
|
+
const now = Date.now();
|
|
171
|
+
this.rateLimitState.requests.push(now);
|
|
172
|
+
this.rateLimitState.dailyRequests++;
|
|
173
|
+
}
|
|
174
|
+
async chat(messages, options) {
|
|
175
|
+
await this.refreshProviderIfNeeded();
|
|
176
|
+
// Check semantic cache first (if enabled)
|
|
177
|
+
// Semantic cache is more powerful - matches similar queries, not just exact matches
|
|
178
|
+
if (this.semanticCache) {
|
|
179
|
+
const query = this.semanticCache.extractQuery(messages);
|
|
180
|
+
const cached = await this.semanticCache.get(query);
|
|
181
|
+
if (cached) {
|
|
182
|
+
// Semantic cache hit - return immediately without API call
|
|
183
|
+
return {
|
|
184
|
+
content: cached.response,
|
|
185
|
+
role: 'assistant',
|
|
186
|
+
finishReason: 'stop',
|
|
187
|
+
usage: cached.usage
|
|
188
|
+
? {
|
|
189
|
+
...cached.usage,
|
|
190
|
+
// Mark as cached for monitoring
|
|
191
|
+
cacheReadTokens: cached.usage.totalTokens,
|
|
192
|
+
}
|
|
193
|
+
: undefined,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
// Check response cache (if enabled and semantic cache didn't hit)
|
|
198
|
+
if (this.responseCache) {
|
|
199
|
+
const cacheKey = this.responseCache.getCacheKey(messages, {
|
|
200
|
+
temperature: options?.temperature,
|
|
201
|
+
maxTokens: options?.maxTokens,
|
|
202
|
+
tools: options?.tools,
|
|
203
|
+
model: this.config.model,
|
|
204
|
+
});
|
|
205
|
+
const cached = this.responseCache.get(cacheKey);
|
|
206
|
+
if (cached) {
|
|
207
|
+
// Cache hit - return immediately without API call
|
|
208
|
+
return {
|
|
209
|
+
...cached,
|
|
210
|
+
usage: cached.usage
|
|
211
|
+
? {
|
|
212
|
+
...cached.usage,
|
|
213
|
+
// Mark as cached for monitoring
|
|
214
|
+
cacheReadTokens: cached.usage.totalTokens,
|
|
215
|
+
}
|
|
216
|
+
: undefined,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
// Cache miss - proceed with API call
|
|
220
|
+
}
|
|
221
|
+
if (!this.checkRateLimit()) {
|
|
222
|
+
throw new Error('Rate limit exceeded');
|
|
223
|
+
}
|
|
224
|
+
const callStart = Date.now();
|
|
225
|
+
try {
|
|
226
|
+
this.recordRequest();
|
|
227
|
+
const response = await this.provider.chat(messages, options);
|
|
228
|
+
this.healthMonitor?.recordCall(this.config.provider, Date.now() - callStart);
|
|
229
|
+
// Store in semantic cache (if enabled)
|
|
230
|
+
if (this.semanticCache) {
|
|
231
|
+
const query = this.semanticCache.extractQuery(messages);
|
|
232
|
+
await this.semanticCache.set(query, response.content, response.usage);
|
|
233
|
+
}
|
|
234
|
+
// Store in response cache (if enabled)
|
|
235
|
+
if (this.responseCache) {
|
|
236
|
+
const cacheKey = this.responseCache.getCacheKey(messages, {
|
|
237
|
+
temperature: options?.temperature,
|
|
238
|
+
maxTokens: options?.maxTokens,
|
|
239
|
+
tools: options?.tools,
|
|
240
|
+
model: this.config.model,
|
|
241
|
+
});
|
|
242
|
+
this.responseCache.set(cacheKey, {
|
|
243
|
+
content: response.content,
|
|
244
|
+
role: response.role,
|
|
245
|
+
finishReason: response.finishReason,
|
|
246
|
+
toolCalls: response.toolCalls,
|
|
247
|
+
timestamp: Date.now(),
|
|
248
|
+
usage: response.usage,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
return response;
|
|
252
|
+
}
|
|
253
|
+
catch (error) {
|
|
254
|
+
this.healthMonitor?.recordCall(this.config.provider, Date.now() - callStart, error instanceof Error ? error : new Error(String(error)));
|
|
255
|
+
// Try fallback if available
|
|
256
|
+
if (this.fallbackProvider && this.config.fallbackProvider) {
|
|
257
|
+
const fallbackStart = Date.now();
|
|
258
|
+
try {
|
|
259
|
+
const fallbackResponse = await this.fallbackProvider.chat(messages, options);
|
|
260
|
+
this.healthMonitor?.recordCall(this.config.fallbackProvider, Date.now() - fallbackStart);
|
|
261
|
+
return fallbackResponse;
|
|
262
|
+
}
|
|
263
|
+
catch (fallbackError) {
|
|
264
|
+
this.healthMonitor?.recordCall(this.config.fallbackProvider, Date.now() - fallbackStart, fallbackError instanceof Error ? fallbackError : new Error(String(fallbackError)));
|
|
265
|
+
throw new Error(`Both primary and fallback providers failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
throw error;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
async embed(text, options) {
|
|
272
|
+
await this.refreshProviderIfNeeded();
|
|
273
|
+
if (!this.checkRateLimit()) {
|
|
274
|
+
throw new Error('Rate limit exceeded');
|
|
275
|
+
}
|
|
276
|
+
try {
|
|
277
|
+
this.recordRequest();
|
|
278
|
+
return await this.provider.embed(text, options);
|
|
279
|
+
}
|
|
280
|
+
catch (error) {
|
|
281
|
+
// Try fallback if available
|
|
282
|
+
if (this.fallbackProvider) {
|
|
283
|
+
try {
|
|
284
|
+
return await this.fallbackProvider.embed(text, options);
|
|
285
|
+
}
|
|
286
|
+
catch {
|
|
287
|
+
throw new Error(`Both primary and fallback providers failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
throw error;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
async *stream(messages, options) {
|
|
294
|
+
await this.refreshProviderIfNeeded();
|
|
295
|
+
// Note: Streaming is not cached (can't cache partial responses)
|
|
296
|
+
if (!this.checkRateLimit()) {
|
|
297
|
+
throw new Error('Rate limit exceeded');
|
|
298
|
+
}
|
|
299
|
+
try {
|
|
300
|
+
this.recordRequest();
|
|
301
|
+
yield* this.provider.stream(messages, options);
|
|
302
|
+
}
|
|
303
|
+
catch (error) {
|
|
304
|
+
// Try fallback if available
|
|
305
|
+
if (this.fallbackProvider) {
|
|
306
|
+
try {
|
|
307
|
+
yield* this.fallbackProvider.stream(messages, options);
|
|
308
|
+
}
|
|
309
|
+
catch {
|
|
310
|
+
throw new Error(`Both primary and fallback providers failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
else {
|
|
314
|
+
throw error;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Estimate token count and cost for a set of messages using the configured model.
|
|
320
|
+
* Uses a heuristic (~4 chars/token). Useful for pre-flight cost checks.
|
|
321
|
+
*/
|
|
322
|
+
estimateRequest(messages) {
|
|
323
|
+
return _estimateRequestTokens(messages, this.config.model ?? '');
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Get the provider health monitor if one was configured.
|
|
327
|
+
*/
|
|
328
|
+
getHealthMonitor() {
|
|
329
|
+
return this.healthMonitor;
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Get response cache statistics
|
|
333
|
+
*
|
|
334
|
+
* @returns Cache stats or undefined if caching is disabled
|
|
335
|
+
*/
|
|
336
|
+
getResponseCacheStats() {
|
|
337
|
+
return this.responseCache?.getStats();
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Clear response cache
|
|
341
|
+
*/
|
|
342
|
+
clearResponseCache() {
|
|
343
|
+
this.responseCache?.clear();
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Get semantic cache statistics
|
|
347
|
+
*
|
|
348
|
+
* @returns Semantic cache stats or undefined if caching is disabled
|
|
349
|
+
*/
|
|
350
|
+
getSemanticCacheStats() {
|
|
351
|
+
return this.semanticCache?.getStats();
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Clear semantic cache
|
|
355
|
+
*/
|
|
356
|
+
clearSemanticCache() {
|
|
357
|
+
this.semanticCache?.resetStats();
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Create an LLM client from environment variables.
|
|
362
|
+
*
|
|
363
|
+
* When LLM_PROVIDER is not set, auto-detects the provider by checking env vars
|
|
364
|
+
* in priority order: GROQ_API_KEY → OLLAMA_BASE_URL → ANTHROPIC_API_KEY.
|
|
365
|
+
*
|
|
366
|
+
* GROQ and Ollama are preferred — they are free-tier and BYOK-friendly.
|
|
367
|
+
* OpenAI is not in the auto-detection chain (no revenue yet — see LLM provider policy).
|
|
368
|
+
* To use OpenAI, set LLM_PROVIDER=openai explicitly.
|
|
369
|
+
*
|
|
370
|
+
* Provider defaults:
|
|
371
|
+
* groq → llama-3.3-70b-versatile
|
|
372
|
+
* ollama → llama3.2:3b
|
|
373
|
+
*/
|
|
374
|
+
export function createLLMClientFromEnv() {
|
|
375
|
+
// Auto-detect provider when LLM_PROVIDER is not explicitly set
|
|
376
|
+
let provider;
|
|
377
|
+
if (process.env.LLM_PROVIDER) {
|
|
378
|
+
provider = process.env.LLM_PROVIDER;
|
|
379
|
+
}
|
|
380
|
+
else if (process.env.GROQ_API_KEY) {
|
|
381
|
+
provider = 'groq';
|
|
382
|
+
}
|
|
383
|
+
else if (process.env.OLLAMA_BASE_URL) {
|
|
384
|
+
provider = 'ollama';
|
|
385
|
+
}
|
|
386
|
+
else if (process.env.ANTHROPIC_API_KEY) {
|
|
387
|
+
provider = 'anthropic';
|
|
388
|
+
}
|
|
389
|
+
else {
|
|
390
|
+
// No provider configured — throw a clear error. OpenAI is intentionally excluded from
|
|
391
|
+
// auto-detection (no revenue yet). Set LLM_PROVIDER=openai explicitly if needed.
|
|
392
|
+
throw new Error('No LLM provider configured. Set one of: GROQ_API_KEY (recommended), ' +
|
|
393
|
+
'OLLAMA_BASE_URL (local), or ANTHROPIC_API_KEY. ' +
|
|
394
|
+
'Alternatively, set LLM_PROVIDER explicitly.');
|
|
395
|
+
}
|
|
396
|
+
let apiKey;
|
|
397
|
+
let baseURL;
|
|
398
|
+
let defaultModel;
|
|
399
|
+
if (provider === 'openai') {
|
|
400
|
+
apiKey = process.env.OPENAI_API_KEY;
|
|
401
|
+
baseURL = process.env.OPENAI_BASE_URL;
|
|
402
|
+
}
|
|
403
|
+
else if (provider === 'anthropic') {
|
|
404
|
+
apiKey = process.env.ANTHROPIC_API_KEY;
|
|
405
|
+
baseURL = process.env.ANTHROPIC_BASE_URL;
|
|
406
|
+
}
|
|
407
|
+
else if (provider === 'vultr') {
|
|
408
|
+
apiKey = process.env.VULTR_API_KEY;
|
|
409
|
+
baseURL = process.env.VULTR_BASE_URL;
|
|
410
|
+
}
|
|
411
|
+
else if (provider === 'huggingface') {
|
|
412
|
+
apiKey = process.env.HF_TOKEN;
|
|
413
|
+
baseURL = process.env.HF_MODEL_URL;
|
|
414
|
+
}
|
|
415
|
+
else if (provider === 'groq') {
|
|
416
|
+
apiKey = process.env.GROQ_API_KEY;
|
|
417
|
+
baseURL = process.env.GROQ_BASE_URL;
|
|
418
|
+
defaultModel = 'llama-3.3-70b-versatile';
|
|
419
|
+
}
|
|
420
|
+
else if (provider === 'ollama') {
|
|
421
|
+
apiKey = 'ollama'; // Ollama ignores the API key
|
|
422
|
+
baseURL = process.env.OLLAMA_BASE_URL;
|
|
423
|
+
defaultModel = 'llama3.2:3b';
|
|
424
|
+
}
|
|
425
|
+
if (!apiKey) {
|
|
426
|
+
throw new Error(`API key not found for provider "${provider}". Set the corresponding env var ` +
|
|
427
|
+
`(GROQ_API_KEY, OLLAMA_BASE_URL, ANTHROPIC_API_KEY, or OPENAI_API_KEY).`);
|
|
428
|
+
}
|
|
429
|
+
return new LLMClient({
|
|
430
|
+
provider,
|
|
431
|
+
apiKey,
|
|
432
|
+
baseURL,
|
|
433
|
+
model: process.env.LLM_MODEL ?? defaultModel,
|
|
434
|
+
temperature: process.env.LLM_TEMPERATURE ? parseFloat(process.env.LLM_TEMPERATURE) : undefined,
|
|
435
|
+
maxTokens: process.env.LLM_MAX_TOKENS ? parseInt(process.env.LLM_MAX_TOKENS, 10) : undefined,
|
|
436
|
+
enableCacheByDefault: process.env.LLM_ENABLE_CACHE === 'true' || process.env.ANTHROPIC_ENABLE_CACHE === 'true',
|
|
437
|
+
enableResponseCache: process.env.LLM_ENABLE_RESPONSE_CACHE === 'true' ||
|
|
438
|
+
process.env.RESPONSE_CACHE_ENABLED === 'true',
|
|
439
|
+
enableSemanticCache: process.env.LLM_ENABLE_SEMANTIC_CACHE === 'true' ||
|
|
440
|
+
process.env.SEMANTIC_CACHE_ENABLED === 'true',
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Create an LLM client using a user's stored BYOK API key.
|
|
445
|
+
*
|
|
446
|
+
* Looks up the user's preferred provider from `tenant_provider_configs`
|
|
447
|
+
* (falling back to the first key in `user_api_keys`), decrypts the key
|
|
448
|
+
* with AES-256-GCM, and returns a configured LLMClient.
|
|
449
|
+
*
|
|
450
|
+
* Returns `null` if the user has no stored keys (callers should fall back
|
|
451
|
+
* to `createLLMClientFromEnv()` or return a 402/feature-unavailable error).
|
|
452
|
+
*
|
|
453
|
+
* @param userId - The user's ID from the `users` table
|
|
454
|
+
* @param db - A Drizzle NeonDB client instance
|
|
455
|
+
*/
|
|
456
|
+
export async function createLLMClientForUser(userId, db, auditStore) {
|
|
457
|
+
// Find the user's preferred provider config
|
|
458
|
+
const [preferredConfig] = await db
|
|
459
|
+
.select()
|
|
460
|
+
.from(tenantProviderConfigs)
|
|
461
|
+
.where(and(eq(tenantProviderConfigs.userId, userId), eq(tenantProviderConfigs.isDefault, true)))
|
|
462
|
+
.limit(1);
|
|
463
|
+
// Find the matching API key (preferred provider, or any available key)
|
|
464
|
+
const keyQuery = db
|
|
465
|
+
.select()
|
|
466
|
+
.from(userApiKeys)
|
|
467
|
+
.where(preferredConfig
|
|
468
|
+
? and(eq(userApiKeys.userId, userId), eq(userApiKeys.provider, preferredConfig.provider))
|
|
469
|
+
: eq(userApiKeys.userId, userId))
|
|
470
|
+
.limit(1);
|
|
471
|
+
const [keyRow] = await keyQuery;
|
|
472
|
+
if (!keyRow)
|
|
473
|
+
return null;
|
|
474
|
+
const plaintext = decryptApiKey(keyRow.encryptedKey);
|
|
475
|
+
const provider = keyRow.provider;
|
|
476
|
+
const model = preferredConfig?.model ?? undefined;
|
|
477
|
+
// Fire-and-forget: record when this key was last used (best-effort, never blocks)
|
|
478
|
+
db.update(userApiKeys)
|
|
479
|
+
.set({ lastUsedAt: new Date() })
|
|
480
|
+
.where(eq(userApiKeys.id, keyRow.id))
|
|
481
|
+
.catch(() => undefined);
|
|
482
|
+
// Fire-and-forget: emit BYOK audit event if an audit store is wired up
|
|
483
|
+
if (auditStore) {
|
|
484
|
+
auditStore
|
|
485
|
+
.append({
|
|
486
|
+
id: crypto.randomUUID(),
|
|
487
|
+
timestamp: new Date(),
|
|
488
|
+
eventType: 'byok:key:accessed',
|
|
489
|
+
severity: 'info',
|
|
490
|
+
agentId: 'system',
|
|
491
|
+
payload: { userId, provider, keyId: keyRow.id },
|
|
492
|
+
policyViolations: [],
|
|
493
|
+
})
|
|
494
|
+
.catch(() => undefined);
|
|
495
|
+
}
|
|
496
|
+
return new LLMClient({ provider, apiKey: plaintext, model });
|
|
497
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BYOK Provider Key Validator
|
|
3
|
+
*
|
|
4
|
+
* Validates API keys against their provider before storage.
|
|
5
|
+
* Uses the cheapest available endpoint for each provider — typically a
|
|
6
|
+
* models list (read-only, no token cost). Falls back gracefully when the
|
|
7
|
+
* provider is unreachable so that network failures don't block key storage.
|
|
8
|
+
*/
|
|
9
|
+
export type ProviderValidationResult = {
|
|
10
|
+
valid: true;
|
|
11
|
+
} | {
|
|
12
|
+
valid: false;
|
|
13
|
+
error: string;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Validate an API key against the given provider.
|
|
17
|
+
*
|
|
18
|
+
* Unreachable providers return `{ valid: true }` with a warning comment so
|
|
19
|
+
* that network outages (especially for Ollama) never block key storage.
|
|
20
|
+
*
|
|
21
|
+
* @param provider - One of the allowed BYOK provider identifiers
|
|
22
|
+
* @param apiKey - The plaintext key to probe
|
|
23
|
+
*/
|
|
24
|
+
export declare function validateProviderKey(provider: string, apiKey: string): Promise<ProviderValidationResult>;
|
|
25
|
+
//# sourceMappingURL=key-validator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"key-validator.d.ts","sourceRoot":"","sources":["../../src/llm/key-validator.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,MAAM,MAAM,wBAAwB,GAAG;IAAE,KAAK,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,KAAK,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAA;AAexF;;;;;;;;GAQG;AACH,wBAAsB,mBAAmB,CACvC,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,wBAAwB,CAAC,CAuEnC"}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BYOK Provider Key Validator
|
|
3
|
+
*
|
|
4
|
+
* Validates API keys against their provider before storage.
|
|
5
|
+
* Uses the cheapest available endpoint for each provider — typically a
|
|
6
|
+
* models list (read-only, no token cost). Falls back gracefully when the
|
|
7
|
+
* provider is unreachable so that network failures don't block key storage.
|
|
8
|
+
*/
|
|
9
|
+
/** Timeout for validation probes (ms). Kept short to avoid blocking the request. */
|
|
10
|
+
const VALIDATION_TIMEOUT_MS = 4_000;
|
|
11
|
+
async function probeFetch(url, init) {
|
|
12
|
+
const controller = new AbortController();
|
|
13
|
+
const timeoutId = setTimeout(() => controller.abort(), VALIDATION_TIMEOUT_MS);
|
|
14
|
+
try {
|
|
15
|
+
return await fetch(url, { ...init, signal: controller.signal });
|
|
16
|
+
}
|
|
17
|
+
finally {
|
|
18
|
+
clearTimeout(timeoutId);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Validate an API key against the given provider.
|
|
23
|
+
*
|
|
24
|
+
* Unreachable providers return `{ valid: true }` with a warning comment so
|
|
25
|
+
* that network outages (especially for Ollama) never block key storage.
|
|
26
|
+
*
|
|
27
|
+
* @param provider - One of the allowed BYOK provider identifiers
|
|
28
|
+
* @param apiKey - The plaintext key to probe
|
|
29
|
+
*/
|
|
30
|
+
export async function validateProviderKey(provider, apiKey) {
|
|
31
|
+
try {
|
|
32
|
+
switch (provider) {
|
|
33
|
+
case 'groq': {
|
|
34
|
+
const res = await probeFetch('https://api.groq.com/openai/v1/models', {
|
|
35
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
36
|
+
});
|
|
37
|
+
if (res.ok)
|
|
38
|
+
return { valid: true };
|
|
39
|
+
if (res.status === 401)
|
|
40
|
+
return { valid: false, error: 'Invalid Groq API key' };
|
|
41
|
+
// Any other non-OK status (429, 500 etc.) — treat as reachable but unknown
|
|
42
|
+
return { valid: false, error: `Groq validation failed: HTTP ${res.status}` };
|
|
43
|
+
}
|
|
44
|
+
case 'anthropic': {
|
|
45
|
+
// Anthropic has no free read endpoint. Validate by key format only.
|
|
46
|
+
// Valid Anthropic keys start with "sk-ant-api".
|
|
47
|
+
if (!apiKey.startsWith('sk-ant-api')) {
|
|
48
|
+
return { valid: false, error: 'Anthropic API key must start with "sk-ant-api"' };
|
|
49
|
+
}
|
|
50
|
+
return { valid: true };
|
|
51
|
+
}
|
|
52
|
+
case 'openai': {
|
|
53
|
+
// Validate by format — keys start with "sk-"
|
|
54
|
+
// (Per LLM policy, OpenAI API calls are blocked until we have revenue.)
|
|
55
|
+
if (!apiKey.startsWith('sk-')) {
|
|
56
|
+
return { valid: false, error: 'OpenAI API key must start with "sk-"' };
|
|
57
|
+
}
|
|
58
|
+
return { valid: true };
|
|
59
|
+
}
|
|
60
|
+
case 'huggingface': {
|
|
61
|
+
const res = await probeFetch('https://huggingface.co/api/whoami-v2', {
|
|
62
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
63
|
+
});
|
|
64
|
+
if (res.ok)
|
|
65
|
+
return { valid: true };
|
|
66
|
+
if (res.status === 401 || res.status === 403) {
|
|
67
|
+
return { valid: false, error: 'Invalid HuggingFace token' };
|
|
68
|
+
}
|
|
69
|
+
return { valid: false, error: `HuggingFace validation failed: HTTP ${res.status}` };
|
|
70
|
+
}
|
|
71
|
+
case 'vultr': {
|
|
72
|
+
// Vultr Serverless Inference API (OpenAI-compatible)
|
|
73
|
+
const res = await probeFetch('https://api.vultrinference.com/v1/models', {
|
|
74
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
75
|
+
});
|
|
76
|
+
if (res.ok)
|
|
77
|
+
return { valid: true };
|
|
78
|
+
if (res.status === 401)
|
|
79
|
+
return { valid: false, error: 'Invalid Vultr API key' };
|
|
80
|
+
return { valid: false, error: `Vultr validation failed: HTTP ${res.status}` };
|
|
81
|
+
}
|
|
82
|
+
case 'ollama': {
|
|
83
|
+
// Ollama is local — we cannot reliably probe it from the server.
|
|
84
|
+
// Accept the key as-is (Ollama doesn't use API keys anyway).
|
|
85
|
+
return { valid: true };
|
|
86
|
+
}
|
|
87
|
+
default:
|
|
88
|
+
// Unknown provider — skip validation
|
|
89
|
+
return { valid: true };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
// Network error (AbortError, DNS failure, etc.) — don't block storage
|
|
94
|
+
if (err instanceof Error && err.name === 'AbortError') {
|
|
95
|
+
// Timeout — provider unreachable, proceed with storage
|
|
96
|
+
return { valid: true };
|
|
97
|
+
}
|
|
98
|
+
// Other network errors (ECONNREFUSED, etc.) — proceed with storage
|
|
99
|
+
return { valid: true };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider Health Monitor
|
|
3
|
+
*
|
|
4
|
+
* Sliding window of last 100 calls per provider.
|
|
5
|
+
* Tracks latency (p50) and error rate to rank fallback candidates.
|
|
6
|
+
*
|
|
7
|
+
* AnythingLLM lesson: no health checks → silent failures cascade.
|
|
8
|
+
*/
|
|
9
|
+
import type { LLMProviderType } from './client.js';
|
|
10
|
+
export interface ProviderHealth {
|
|
11
|
+
provider: LLMProviderType;
|
|
12
|
+
/** healthy | degraded | unhealthy */
|
|
13
|
+
status: 'healthy' | 'degraded' | 'unhealthy';
|
|
14
|
+
latencyP50Ms: number;
|
|
15
|
+
errorRate: number;
|
|
16
|
+
sampleCount: number;
|
|
17
|
+
}
|
|
18
|
+
export declare class ProviderHealthMonitor {
|
|
19
|
+
private windows;
|
|
20
|
+
private getWindow;
|
|
21
|
+
/**
|
|
22
|
+
* Record a completed LLM API call.
|
|
23
|
+
*/
|
|
24
|
+
recordCall(provider: LLMProviderType, latencyMs: number, error?: Error | null): void;
|
|
25
|
+
/**
|
|
26
|
+
* Get health metrics for a provider.
|
|
27
|
+
* Returns 'healthy' with 0 latency if no calls recorded yet.
|
|
28
|
+
*/
|
|
29
|
+
getHealth(provider: LLMProviderType): ProviderHealth;
|
|
30
|
+
/**
|
|
31
|
+
* Pick the best provider from a set of candidates.
|
|
32
|
+
* Prefers healthy > degraded > unhealthy, then by p50 latency.
|
|
33
|
+
*/
|
|
34
|
+
getBestProvider(candidates: LLMProviderType[]): LLMProviderType;
|
|
35
|
+
/**
|
|
36
|
+
* Reset health data for a provider (useful for testing).
|
|
37
|
+
*/
|
|
38
|
+
reset(provider: LLMProviderType): void;
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=provider-health.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provider-health.d.ts","sourceRoot":"","sources":["../../src/llm/provider-health.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAElD,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,eAAe,CAAA;IACzB,qCAAqC;IACrC,MAAM,EAAE,SAAS,GAAG,UAAU,GAAG,WAAW,CAAA;IAC5C,YAAY,EAAE,MAAM,CAAA;IACpB,SAAS,EAAE,MAAM,CAAA;IACjB,WAAW,EAAE,MAAM,CAAA;CACpB;AAqBD,qBAAa,qBAAqB;IAChC,OAAO,CAAC,OAAO,CAAgD;IAE/D,OAAO,CAAC,SAAS;IAQjB;;OAEG;IACH,UAAU,CAAC,QAAQ,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,KAAK,GAAG,IAAI,GAAG,IAAI;IAUpF;;;OAGG;IACH,SAAS,CAAC,QAAQ,EAAE,eAAe,GAAG,cAAc;IA4BpD;;;OAGG;IACH,eAAe,CAAC,UAAU,EAAE,eAAe,EAAE,GAAG,eAAe;IAkB/D;;OAEG;IACH,KAAK,CAAC,QAAQ,EAAE,eAAe,GAAG,IAAI;CAGvC"}
|