openclaw-hybrid-memory 2026.5.310 → 2026.6.10
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/api/plugin-runtime.ts +2 -0
- package/backends/facts-db/contradictions.ts +1 -1
- package/cli/cmd-extract-directives.ts +225 -11
- package/cli/cmd-extract-proposals.ts +5 -6
- package/cli/cmd-extract-reinforcement.ts +71 -0
- package/cli/cmd-feedback.ts +15 -9
- package/cli/commands/manage/register-reflection-pipeline.ts +247 -13
- package/cli/commands/manage/register-storage-maintenance.ts +224 -15
- package/cli/commands/manage/storage-stats-helpers.ts +13 -2
- package/cli/context.ts +9 -19
- package/cli/distill.ts +31 -1
- package/cli/register.ts +28 -38
- package/dist/api/plugin-runtime.js.map +1 -1
- package/dist/backends/agent-health-store.js.map +1 -1
- package/dist/backends/apitap-store.js.map +1 -1
- package/dist/backends/audit-store.js.map +1 -1
- package/dist/backends/base-sqlite-store.js.map +1 -1
- package/dist/backends/cost-tracker.js.map +1 -1
- package/dist/backends/credentials-db.js +2 -3
- package/dist/backends/credentials-db.js.map +1 -1
- package/dist/backends/crystallization-store.js.map +1 -1
- package/dist/backends/edict-store.js.map +1 -1
- package/dist/backends/event-bus.js.map +1 -1
- package/dist/backends/event-log.js.map +1 -1
- package/dist/backends/facts-db/cache-manager.js.map +1 -1
- package/dist/backends/facts-db/clusters.js.map +1 -1
- package/dist/backends/facts-db/contradictions.js +1 -1
- package/dist/backends/facts-db/contradictions.js.map +1 -1
- package/dist/backends/facts-db/crud.js.map +1 -1
- package/dist/backends/facts-db/db-connection.js.map +1 -1
- package/dist/backends/facts-db/entity-autolink.js.map +1 -1
- package/dist/backends/facts-db/entity-layer.js.map +1 -1
- package/dist/backends/facts-db/episodes.js.map +1 -1
- package/dist/backends/facts-db/fact-queries.js.map +1 -1
- package/dist/backends/facts-db/fact-read-queries.js.map +1 -1
- package/dist/backends/facts-db/facts-db-layer1.js.map +1 -1
- package/dist/backends/facts-db/facts-db-layer2.js.map +1 -1
- package/dist/backends/facts-db/facts-db-layer3.js.map +1 -1
- package/dist/backends/facts-db/fts-text.js.map +1 -1
- package/dist/backends/facts-db/generated-skills/policy.js.map +1 -1
- package/dist/backends/facts-db/generated-skills.js.map +1 -1
- package/dist/backends/facts-db/housekeeping.js.map +1 -1
- package/dist/backends/facts-db/links.js.map +1 -1
- package/dist/backends/facts-db/maintenance.js.map +1 -1
- package/dist/backends/facts-db/procedures/crud.js.map +1 -1
- package/dist/backends/facts-db/procedures/internal.js.map +1 -1
- package/dist/backends/facts-db/procedures/promotion.js.map +1 -1
- package/dist/backends/facts-db/procedures/search.js.map +1 -1
- package/dist/backends/facts-db/procedures/stats.js.map +1 -1
- package/dist/backends/facts-db/reinforcement.js.map +1 -1
- package/dist/backends/facts-db/row-mapper.js.map +1 -1
- package/dist/backends/facts-db/scan-cursors.js.map +1 -1
- package/dist/backends/facts-db/schema-bootstrap.js.map +1 -1
- package/dist/backends/facts-db/scope-sql.js.map +1 -1
- package/dist/backends/facts-db/search.js.map +1 -1
- package/dist/backends/facts-db/stats.js.map +1 -1
- package/dist/backends/facts-db/types.js.map +1 -1
- package/dist/backends/facts-db/variants.js.map +1 -1
- package/dist/backends/identity-reflection-store.js.map +1 -1
- package/dist/backends/issue-store.js.map +1 -1
- package/dist/backends/learnings-db.js.map +1 -1
- package/dist/backends/migrations/facts-migrations.js.map +1 -1
- package/dist/backends/migrations/procedures.js.map +1 -1
- package/dist/backends/narratives-db.js.map +1 -1
- package/dist/backends/persona-state-store.js.map +1 -1
- package/dist/backends/proposals-db.js.map +1 -1
- package/dist/backends/scope-filter-sql.js.map +1 -1
- package/dist/backends/sqlite-schema-meta.js.map +1 -1
- package/dist/backends/tool-proposal-store.js.map +1 -1
- package/dist/backends/vector-db/constants.js.map +1 -1
- package/dist/backends/vector-db/path-utils.js.map +1 -1
- package/dist/backends/vector-db/runtime-locks.js.map +1 -1
- package/dist/backends/vector-db/vector-db-class.js.map +1 -1
- package/dist/backends/wal.js.map +1 -1
- package/dist/backends/workflow-store.js.map +1 -1
- package/dist/benchmark/shadow-eval.js.map +1 -1
- package/dist/cli/active-tasks.js.map +1 -1
- package/dist/cli/backup.js.map +1 -1
- package/dist/cli/benchmark.js.map +1 -1
- package/dist/cli/cmd-backfill.js.map +1 -1
- package/dist/cli/cmd-config.js.map +1 -1
- package/dist/cli/cmd-credentials.js.map +1 -1
- package/dist/cli/cmd-demo.js.map +1 -1
- package/dist/cli/cmd-distill.js.map +1 -1
- package/dist/cli/cmd-doctor.js.map +1 -1
- package/dist/cli/cmd-examples.js.map +1 -1
- package/dist/cli/cmd-extract-daily.js.map +1 -1
- package/dist/cli/cmd-extract-directives.js +141 -10
- package/dist/cli/cmd-extract-directives.js.map +1 -1
- package/dist/cli/cmd-extract-procedures.js.map +1 -1
- package/dist/cli/cmd-extract-proposals.js +3 -2
- package/dist/cli/cmd-extract-proposals.js.map +1 -1
- package/dist/cli/cmd-extract-reinforcement.js +39 -0
- package/dist/cli/cmd-extract-reinforcement.js.map +1 -1
- package/dist/cli/cmd-extract-sessions.js.map +1 -1
- package/dist/cli/cmd-feedback.js +9 -4
- package/dist/cli/cmd-feedback.js.map +1 -1
- package/dist/cli/cmd-health.js.map +1 -1
- package/dist/cli/cmd-providers.js.map +1 -1
- package/dist/cli/cmd-selfcorrection.js.map +1 -1
- package/dist/cli/cmd-setup.js.map +1 -1
- package/dist/cli/cmd-status.js.map +1 -1
- package/dist/cli/cmd-store.js.map +1 -1
- package/dist/cli/cmd-user-friendly.js.map +1 -1
- package/dist/cli/cmd-verify.js.map +1 -1
- package/dist/cli/commands/manage/bindings.js.map +1 -1
- package/dist/cli/commands/manage/dream-cycle-followup.js.map +1 -1
- package/dist/cli/commands/manage/maintenance-heartbeat.js.map +1 -1
- package/dist/cli/commands/manage/register-agents-audit-runall.js.map +1 -1
- package/dist/cli/commands/manage/register-analyze-maintenance-logs.js.map +1 -1
- package/dist/cli/commands/manage/register-budget-proposals.js.map +1 -1
- package/dist/cli/commands/manage/register-config-cli.js.map +1 -1
- package/dist/cli/commands/manage/register-corrections-and-pipeline.js.map +1 -1
- package/dist/cli/commands/manage/register-corrections.js.map +1 -1
- package/dist/cli/commands/manage/register-council.js.map +1 -1
- package/dist/cli/commands/manage/register-credentials-scope.js.map +1 -1
- package/dist/cli/commands/manage/register-digest.js.map +1 -1
- package/dist/cli/commands/manage/register-lifecycle.js.map +1 -1
- package/dist/cli/commands/manage/register-procedure-lifecycle.js.map +1 -1
- package/dist/cli/commands/manage/register-reconcile-cron-ledgers.js.map +1 -1
- package/dist/cli/commands/manage/register-reflection-pipeline.js +144 -7
- package/dist/cli/commands/manage/register-reflection-pipeline.js.map +1 -1
- package/dist/cli/commands/manage/register-self-correction-feedback.js.map +1 -1
- package/dist/cli/commands/manage/register-storage-and-stats.js.map +1 -1
- package/dist/cli/commands/manage/register-storage-entities-decay.js.map +1 -1
- package/dist/cli/commands/manage/register-storage-graph-audit.js.map +1 -1
- package/dist/cli/commands/manage/register-storage-maintenance.js +152 -9
- package/dist/cli/commands/manage/register-storage-maintenance.js.map +1 -1
- package/dist/cli/commands/manage/register-validate-cron-exit.js.map +1 -1
- package/dist/cli/commands/manage/storage-stats-helpers.js +10 -3
- package/dist/cli/commands/manage/storage-stats-helpers.js.map +1 -1
- package/dist/cli/commands/register-manage-commands.js.map +1 -1
- package/dist/cli/config-feature-summaries.js.map +1 -1
- package/dist/cli/config-output-sink.js.map +1 -1
- package/dist/cli/distill-session-jsonl.js.map +1 -1
- package/dist/cli/distill.js +10 -1
- package/dist/cli/distill.js.map +1 -1
- package/dist/cli/global-verbose.js.map +1 -1
- package/dist/cli/goals.js.map +1 -1
- package/dist/cli/hybrid-mem-commander-utils.js.map +1 -1
- package/dist/cli/install/config-merge.js.map +1 -1
- package/dist/cli/install/cron-jobs.js.map +1 -1
- package/dist/cli/install/embedding-detect.js.map +1 -1
- package/dist/cli/install/run-install.js.map +1 -1
- package/dist/cli/install/workspace.js.map +1 -1
- package/dist/cli/proposals.js.map +1 -1
- package/dist/cli/register.js.map +1 -1
- package/dist/cli/shared.js.map +1 -1
- package/dist/cli/skills.js.map +1 -1
- package/dist/cli/task-queue-status.js.map +1 -1
- package/dist/cli/verified.js.map +1 -1
- package/dist/cli/verify/fact-count.js.map +1 -1
- package/dist/cli/verify/openclaw-config.js.map +1 -1
- package/dist/cli/verify/plugin-config-credentials.js.map +1 -1
- package/dist/cli/verify/sections/config-cron.js.map +1 -1
- package/dist/cli/verify/sections/embeddings.js.map +1 -1
- package/dist/cli/verify/sections/infrastructure.js.map +1 -1
- package/dist/cli/verify/sections/llm-models.js.map +1 -1
- package/dist/cli/verify/sections/reconcile.js.map +1 -1
- package/dist/cli/verify/verify-run-state.js.map +1 -1
- package/dist/cli/verify-llm-azure-auth.js.map +1 -1
- package/dist/cli/verify.js.map +1 -1
- package/dist/config/hybrid-schema.js.map +1 -1
- package/dist/config/index.js.map +1 -1
- package/dist/config/maintenance-fallback-policy.js.map +1 -1
- package/dist/config/parsers/capture.js.map +1 -1
- package/dist/config/parsers/core.js.map +1 -1
- package/dist/config/parsers/features.js.map +1 -1
- package/dist/config/parsers/index.js.map +1 -1
- package/dist/config/parsers/maintenance.js.map +1 -1
- package/dist/config/parsers/retrieval.js.map +1 -1
- package/dist/config/parsers/sensors.js.map +1 -1
- package/dist/config/skill-sections.js.map +1 -1
- package/dist/config/skill-size-limits.js.map +1 -1
- package/dist/config/types/agents.js.map +1 -1
- package/dist/config/types/bootstrap.js.map +1 -1
- package/dist/config/types/core.js.map +1 -1
- package/dist/config/types/index.js.map +1 -1
- package/dist/config/utils.js.map +1 -1
- package/dist/index-help.js.map +1 -1
- package/dist/index-testing-exports.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/lifecycle/hook-resolution-api.js.map +1 -1
- package/dist/lifecycle/hooks.js +0 -1
- package/dist/lifecycle/hooks.js.map +1 -1
- package/dist/lifecycle/resolve-agent-id.js.map +1 -1
- package/dist/lifecycle/session-state.js.map +1 -1
- package/dist/lifecycle/stage-active-task.js.map +1 -1
- package/dist/lifecycle/stage-auth-failure.js.map +1 -1
- package/dist/lifecycle/stage-capture/run-capture.js.map +1 -1
- package/dist/lifecycle/stage-capture.js.map +1 -1
- package/dist/lifecycle/stage-cleanup.js.map +1 -1
- package/dist/lifecycle/stage-credential-hint.js.map +1 -1
- package/dist/lifecycle/stage-frustration.js.map +1 -1
- package/dist/lifecycle/stage-goal-stewardship.js.map +1 -1
- package/dist/lifecycle/stage-goal-subagent.js.map +1 -1
- package/dist/lifecycle/stage-injection.js +1 -1
- package/dist/lifecycle/stage-injection.js.map +1 -1
- package/dist/lifecycle/stage-recall/run-recall.js.map +1 -1
- package/dist/lifecycle/stage-recall.js.map +1 -1
- package/dist/lifecycle/stage-setup.js.map +1 -1
- package/dist/routes/dashboard/collectors.js.map +1 -1
- package/dist/routes/dashboard/html.js.map +1 -1
- package/dist/routes/dashboard/server.js.map +1 -1
- package/dist/routes/dashboard-graph.js.map +1 -1
- package/dist/routes/graphql-resolvers.js.map +1 -1
- package/dist/routes/graphql-server.js.map +1 -1
- package/dist/services/active-task-checkpoint.js.map +1 -1
- package/dist/services/active-task-injection.js.map +1 -1
- package/dist/services/active-task.js.map +1 -1
- package/dist/services/adaptive-catch-up-pacing.js +25 -0
- package/dist/services/adaptive-catch-up-pacing.js.map +1 -0
- package/dist/services/adaptive-maintenance-llm.js.map +1 -1
- package/dist/services/adaptive-model-limits.js.map +1 -1
- package/dist/services/ambient-retrieval.js.map +1 -1
- package/dist/services/apitap-service.js.map +1 -1
- package/dist/services/audit-health-exit-info.js.map +1 -1
- package/dist/services/audit-health-json.js.map +1 -1
- package/dist/services/auth-failure-detect.js.map +1 -1
- package/dist/services/auto-capture.js.map +1 -1
- package/dist/services/auto-classifier.js.map +1 -1
- package/dist/services/auto-skills-audit.js.map +1 -1
- package/dist/services/bootstrap-optional.js.map +1 -1
- package/dist/services/bootstrap-priority.js.map +1 -1
- package/dist/services/bootstrap.js.map +1 -1
- package/dist/services/capture-provenance.js.map +1 -1
- package/dist/services/capture-utils.js.map +1 -1
- package/dist/services/chat.js +22 -3
- package/dist/services/chat.js.map +1 -1
- package/dist/services/classification-scope.js.map +1 -1
- package/dist/services/classification.js.map +1 -1
- package/dist/services/cli-sql-dump.js.map +1 -1
- package/dist/services/consolidation.js.map +1 -1
- package/dist/services/context-audit.js +1 -1
- package/dist/services/context-audit.js.map +1 -1
- package/dist/services/context-budget.js.map +1 -1
- package/dist/services/context-engine.js.map +1 -1
- package/dist/services/contextual-variants.js.map +1 -1
- package/dist/services/continuous-verifier.js.map +1 -1
- package/dist/services/contradiction-adjudicator.js.map +1 -1
- package/dist/services/cost-context.js.map +1 -1
- package/dist/services/cost-feature-labels.js.map +1 -1
- package/dist/services/credential-migration.js.map +1 -1
- package/dist/services/credential-scanner.js.map +1 -1
- package/dist/services/credential-validation.js.map +1 -1
- package/dist/services/cron-exit-validator.js.map +1 -1
- package/dist/services/cron-guard.js.map +1 -1
- package/dist/services/cron-job-bash-harness.js +52 -5
- package/dist/services/cron-job-bash-harness.js.map +1 -1
- package/dist/services/cron-maintenance-reconciler.js +1 -3
- package/dist/services/cron-maintenance-reconciler.js.map +1 -1
- package/dist/services/cross-agent-learning.js.map +1 -1
- package/dist/services/crystallization-proposer.js.map +1 -1
- package/dist/services/dedupe-policy.js.map +1 -1
- package/dist/services/deprecated-cron-commands.js.map +1 -1
- package/dist/services/directive-extract.js.map +1 -1
- package/dist/services/document-chunker.js.map +1 -1
- package/dist/services/document-grader.js.map +1 -1
- package/dist/services/dream-cycle.js.map +1 -1
- package/dist/services/embedding-migration.js.map +1 -1
- package/dist/services/embedding-registry.js.map +1 -1
- package/dist/services/embeddings/chain-provider.js.map +1 -1
- package/dist/services/embeddings/factory.js.map +1 -1
- package/dist/services/embeddings/fallback-provider.js.map +1 -1
- package/dist/services/embeddings/ollama-provider.js.map +1 -1
- package/dist/services/embeddings/onnx-provider.js.map +1 -1
- package/dist/services/embeddings/openai-provider.js.map +1 -1
- package/dist/services/embeddings/shared.js +3 -3
- package/dist/services/embeddings/shared.js.map +1 -1
- package/dist/services/embeddings/types.js.map +1 -1
- package/dist/services/entity-enrichment-adaptive.js +128 -0
- package/dist/services/entity-enrichment-adaptive.js.map +1 -0
- package/dist/services/entity-enrichment-cli.js +389 -42
- package/dist/services/entity-enrichment-cli.js.map +1 -1
- package/dist/services/entity-enrichment.js +31 -5
- package/dist/services/entity-enrichment.js.map +1 -1
- package/dist/services/error-reporter/noisy-errors.js.map +1 -1
- package/dist/services/error-reporter/sanitize.js.map +1 -1
- package/dist/services/error-reporter.js.map +1 -1
- package/dist/services/event-hub-repair.js.map +1 -1
- package/dist/services/export-memory.js.map +1 -1
- package/dist/services/fact-extraction.js.map +1 -1
- package/dist/services/feedback-effectiveness.js.map +1 -1
- package/dist/services/find-duplicates.js.map +1 -1
- package/dist/services/frustration-detector.js.map +1 -1
- package/dist/services/fts-search.js.map +1 -1
- package/dist/services/gap-detector.js.map +1 -1
- package/dist/services/generated-skill-lifecycle.js.map +1 -1
- package/dist/services/generated-skill-validation.js.map +1 -1
- package/dist/services/goal-active-task-mirror.js.map +1 -1
- package/dist/services/goal-circuit-breaker.js.map +1 -1
- package/dist/services/goal-health.js.map +1 -1
- package/dist/services/goal-registry.js.map +1 -1
- package/dist/services/goal-stewardship-heartbeat.js.map +1 -1
- package/dist/services/goal-stewardship-llm-triage.js.map +1 -1
- package/dist/services/goal-stewardship-verify-cron.js.map +1 -1
- package/dist/services/goal-stewardship.js.map +1 -1
- package/dist/services/goal-subagent.js.map +1 -1
- package/dist/services/graph-retrieval.js.map +1 -1
- package/dist/services/humanizer-score.js.map +1 -1
- package/dist/services/hybrid-mem-cron-default-job-steps.js.map +1 -1
- package/dist/services/hyde-helper.js.map +1 -1
- package/dist/services/identity-reflection.js.map +1 -1
- package/dist/services/implicit-feedback-extract.js.map +1 -1
- package/dist/services/index.js.map +1 -1
- package/dist/services/ingest-utils.js.map +1 -1
- package/dist/services/intent-template.js.map +1 -1
- package/dist/services/json-array-parser.js.map +1 -1
- package/dist/services/knowledge-gaps.js.map +1 -1
- package/dist/services/language-keywords-build.js.map +1 -1
- package/dist/services/lifecycle/github-adapter.js.map +1 -1
- package/dist/services/llm-rate-limit-headers.js +1 -2
- package/dist/services/llm-rate-limit-headers.js.map +1 -1
- package/dist/services/maintenance-auto-fix.js.map +1 -1
- package/dist/services/maintenance-log-analyzer.js +7 -1
- package/dist/services/maintenance-log-analyzer.js.map +1 -1
- package/dist/services/maintenance-timestamp.js.map +1 -1
- package/dist/services/memory-diagnostics.js.map +1 -1
- package/dist/services/memory-index.js.map +1 -1
- package/dist/services/merge-results.js.map +1 -1
- package/dist/services/model-capabilities.js.map +1 -1
- package/dist/services/model-pricing.js.map +1 -1
- package/dist/services/narrative-recall.js.map +1 -1
- package/dist/services/openclaw-session-artifact.js.map +1 -1
- package/dist/services/passive-observer.js.map +1 -1
- package/dist/services/pattern-detector-hash.js.map +1 -1
- package/dist/services/pattern-detector.js.map +1 -1
- package/dist/services/pending-autopilot/foundation.js.map +1 -1
- package/dist/services/pending-autopilot/redaction.js.map +1 -1
- package/dist/services/pending-autopilot/store.js.map +1 -1
- package/dist/services/pending-autopilot/types.js.map +1 -1
- package/dist/services/pending-digest-autopilot-cron.js.map +1 -1
- package/dist/services/pending-digest-autopilot.js.map +1 -1
- package/dist/services/pending-review-digest.js.map +1 -1
- package/dist/services/persona-proposal-triage.js.map +1 -1
- package/dist/services/persona-state-promotion.js.map +1 -1
- package/dist/services/post-compaction-recall.js.map +1 -1
- package/dist/services/pre-consolidation-flush.js.map +1 -1
- package/dist/services/pre-finalization-guard.js.map +1 -1
- package/dist/services/procedure-cluster.js.map +1 -1
- package/dist/services/procedure-extractor.js.map +1 -1
- package/dist/services/procedure-promotion/duplicate-skill-cache.js.map +1 -1
- package/dist/services/procedure-promotion-policy.js.map +1 -1
- package/dist/services/procedure-selection-metrics.js.map +1 -1
- package/dist/services/procedure-skill-eval.js.map +1 -1
- package/dist/services/procedure-skill-generator.js.map +1 -1
- package/dist/services/procedure-skill-recipe.js.map +1 -1
- package/dist/services/procedure-skill-shrink.js.map +1 -1
- package/dist/services/procedure-skill-workflow.js.map +1 -1
- package/dist/services/provenance.js.map +1 -1
- package/dist/services/public-export-bundle.js.map +1 -1
- package/dist/services/python-bridge.js.map +1 -1
- package/dist/services/query-expander.js.map +1 -1
- package/dist/services/query-validator.js.map +1 -1
- package/dist/services/recall-pipeline.js.map +1 -1
- package/dist/services/recall-timing.js.map +1 -1
- package/dist/services/recent-http-attempts.js.map +1 -1
- package/dist/services/reflection/shared.js.map +1 -1
- package/dist/services/reflection.js.map +1 -1
- package/dist/services/reinforcement-extract.js.map +1 -1
- package/dist/services/reranker.js.map +1 -1
- package/dist/services/responses-adapter.js.map +1 -1
- package/dist/services/retrieval-aliases.js.map +1 -1
- package/dist/services/retrieval-mode-policy.js.map +1 -1
- package/dist/services/retrieval-orchestrator/packing.js.map +1 -1
- package/dist/services/retrieval-orchestrator.d.ts +2 -3
- package/dist/services/retrieval-orchestrator.js.map +1 -1
- package/dist/services/rrf-fusion.js.map +1 -1
- package/dist/services/self-correction-extract.js.map +1 -1
- package/dist/services/session-observability.js.map +1 -1
- package/dist/services/session-pre-filter.js.map +1 -1
- package/dist/services/shortest-path.js.map +1 -1
- package/dist/services/skill-allowed-tools.js.map +1 -1
- package/dist/services/skill-creator-validator.js.map +1 -1
- package/dist/services/skill-crystallizer-helpers.js.map +1 -1
- package/dist/services/skill-crystallizer.js.map +1 -1
- package/dist/services/skill-description-builder.js.map +1 -1
- package/dist/services/skill-eval-synthesizer.js.map +1 -1
- package/dist/services/skill-examples-builder.js.map +1 -1
- package/dist/services/skill-frontmatter.js.map +1 -1
- package/dist/services/skill-name-validator.js.map +1 -1
- package/dist/services/skill-prompt-injection.js.map +1 -1
- package/dist/services/skill-reference-sidecar.js.map +1 -1
- package/dist/services/skill-script-bundler.js.map +1 -1
- package/dist/services/skill-validator.js.map +1 -1
- package/dist/services/startup-memory-attribution.js.map +1 -1
- package/dist/services/task-hygiene.js.map +1 -1
- package/dist/services/task-ledger/canonical.js.map +1 -1
- package/dist/services/task-ledger-facts.js.map +1 -1
- package/dist/services/task-queue-leases.js.map +1 -1
- package/dist/services/task-queue-watchdog.js.map +1 -1
- package/dist/services/tool-effectiveness.js.map +1 -1
- package/dist/services/tool-proposer.js.map +1 -1
- package/dist/services/tools-md-section.js.map +1 -1
- package/dist/services/topic-clusters.js.map +1 -1
- package/dist/services/trajectory-tracker.js.map +1 -1
- package/dist/services/vector-backend-observability.js.map +1 -1
- package/dist/services/vector-lifecycle-audit.js.map +1 -1
- package/dist/services/vector-maintenance.js.map +1 -1
- package/dist/services/vector-search.js.map +1 -1
- package/dist/services/verification-store.js.map +1 -1
- package/dist/services/verified-fact-triage.js.map +1 -1
- package/dist/services/wal-helpers.js.map +1 -1
- package/dist/services/workflow-tracker.js.map +1 -1
- package/dist/setup/bootstrap-databases.js.map +1 -1
- package/dist/setup/cli-context/cli-services.js.map +1 -1
- package/dist/setup/cli-context/help-text.js.map +1 -1
- package/dist/setup/cli-context/metadata.js.map +1 -1
- package/dist/setup/cli-context/register-cli-with-help.js.map +1 -1
- package/dist/setup/cli-context/register-full.js.map +1 -1
- package/dist/setup/cli-context/register-help.js.map +1 -1
- package/dist/setup/cost-instrumentation.js.map +1 -1
- package/dist/setup/hybrid-memory-generation-state.js.map +1 -1
- package/dist/setup/hybrid-memory-reload-coordinator.js +13 -13
- package/dist/setup/hybrid-memory-reload-coordinator.js.map +1 -1
- package/dist/setup/plugin-service.js.map +1 -1
- package/dist/setup/provider-router.js.map +1 -1
- package/dist/setup/register-context-engine.js.map +1 -1
- package/dist/setup/register-hooks.js.map +1 -1
- package/dist/setup/register-plugin.js +25 -21
- package/dist/setup/register-plugin.js.map +1 -1
- package/dist/setup/register-tools.js.map +1 -1
- package/dist/setup/reregister-policy.js +2 -2
- package/dist/setup/reregister-policy.js.map +1 -1
- package/dist/setup/tool-installers.js.map +1 -1
- package/dist/setup/workspace-bootstrap.js.map +1 -1
- package/dist/src/worker/narratives.js.map +1 -1
- package/dist/tools/apitap-tools.js.map +1 -1
- package/dist/tools/credential-tools.js.map +1 -1
- package/dist/tools/crystallization-tools.js.map +1 -1
- package/dist/tools/dashboard-routes.js.map +1 -1
- package/dist/tools/document-tools.js.map +1 -1
- package/dist/tools/goal-tools.js.map +1 -1
- package/dist/tools/graph-tools.js.map +1 -1
- package/dist/tools/issue-tools.js.map +1 -1
- package/dist/tools/memory/build-runtime.js.map +1 -1
- package/dist/tools/memory/helpers.js.map +1 -1
- package/dist/tools/memory/register-checkpoint-tools.js.map +1 -1
- package/dist/tools/memory/register-directory-tools.js.map +1 -1
- package/dist/tools/memory/register-edict-tools.js.map +1 -1
- package/dist/tools/memory/register-episode-tools.js.map +1 -1
- package/dist/tools/memory/register-recall-tools.js.map +1 -1
- package/dist/tools/memory/register-store-tools.js.map +1 -1
- package/dist/tools/memory-tools.js.map +1 -1
- package/dist/tools/persona-tools.js.map +1 -1
- package/dist/tools/provenance-tools.js.map +1 -1
- package/dist/tools/public-api-routes.js.map +1 -1
- package/dist/tools/safe-register-http-route.js.map +1 -1
- package/dist/tools/self-extension-tools.js.map +1 -1
- package/dist/tools/task-hygiene-tools.js.map +1 -1
- package/dist/tools/utility-tools.js.map +1 -1
- package/dist/tools/verification-tools.js.map +1 -1
- package/dist/tools/workflow-tools.js.map +1 -1
- package/dist/types/issue-types.js.map +1 -1
- package/dist/types/learnings-types.js.map +1 -1
- package/dist/types/memory.js.map +1 -1
- package/dist/utils/apim-gateway-fetch.js.map +1 -1
- package/dist/utils/atomic-write.js.map +1 -1
- package/dist/utils/auth-failover.js.map +1 -1
- package/dist/utils/auth.js.map +1 -1
- package/dist/utils/compaction-model-watchdog.js.map +1 -1
- package/dist/utils/consolidation-controls.js.map +1 -1
- package/dist/utils/constants.js.map +1 -1
- package/dist/utils/date-detector.js.map +1 -1
- package/dist/utils/dates.js.map +1 -1
- package/dist/utils/decay.js.map +1 -1
- package/dist/utils/duration.js.map +1 -1
- package/dist/utils/embed-call.js.map +1 -1
- package/dist/utils/entity-lookup-resolve.js.map +1 -1
- package/dist/utils/entity-mention-quality.js.map +1 -1
- package/dist/utils/entity-stopwords.js.map +1 -1
- package/dist/utils/env-manager.js.map +1 -1
- package/dist/utils/error-tracking.js.map +1 -1
- package/dist/utils/event-loop-yield.js.map +1 -1
- package/dist/utils/extract-last-user-message.js.map +1 -1
- package/dist/utils/extraction-from-template.js.map +1 -1
- package/dist/utils/fact-embeddings.js.map +1 -1
- package/dist/utils/file-snapshot.js.map +1 -1
- package/dist/utils/format.js.map +1 -1
- package/dist/utils/fs.js.map +1 -1
- package/dist/utils/gh-repo-arg.js.map +1 -1
- package/dist/utils/hybrid-mem-json-cli.js.map +1 -1
- package/dist/utils/language-keywords.js.map +1 -1
- package/dist/utils/lifecycle-generation.js.map +1 -1
- package/dist/utils/llm-json-array.js.map +1 -1
- package/dist/utils/llm-selection.js.map +1 -1
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils/model-provider-family.js.map +1 -1
- package/dist/utils/model-tier.js.map +1 -1
- package/dist/utils/openclaw-agent-defaults.js.map +1 -1
- package/dist/utils/path.js.map +1 -1
- package/dist/utils/plugin-root.js.map +1 -1
- package/dist/utils/plugin-update-check.js.map +1 -1
- package/dist/utils/procedure-risk.js.map +1 -1
- package/dist/utils/progress-indicators.js.map +1 -1
- package/dist/utils/prompt-loader.js.map +1 -1
- package/dist/utils/provenance.js.map +1 -1
- package/dist/utils/provider-detection.js.map +1 -1
- package/dist/utils/registration-superseded.js.map +1 -1
- package/dist/utils/salience.js.map +1 -1
- package/dist/utils/sanitize-messages.js.map +1 -1
- package/dist/utils/scope-filter.js.map +1 -1
- package/dist/utils/skill-discovery.js.map +1 -1
- package/dist/utils/sqlite-file-perms.js.map +1 -1
- package/dist/utils/sqlite-outcome-compat.js.map +1 -1
- package/dist/utils/sqlite-transaction.js.map +1 -1
- package/dist/utils/stable-stringify.js.map +1 -1
- package/dist/utils/subagent-ended-utils.js.map +1 -1
- package/dist/utils/tags.js.map +1 -1
- package/dist/utils/text.js.map +1 -1
- package/dist/utils/timeout.js.map +1 -1
- package/dist/utils/typebox.js.map +1 -1
- package/dist/utils/version-check.js.map +1 -1
- package/dist/utils/wal-replay.js.map +1 -1
- package/dist/versionInfo.js.map +1 -1
- package/index.ts +2 -2
- package/lifecycle/hooks.ts +0 -1
- package/npm-shrinkwrap.json +487 -186
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -2
- package/services/adaptive-catch-up-pacing.ts +28 -0
- package/services/chat.ts +34 -1
- package/services/cron-job-bash-harness.ts +52 -5
- package/services/embeddings/shared.ts +5 -2
- package/services/entity-enrichment-adaptive.ts +245 -0
- package/services/entity-enrichment-cli.ts +553 -47
- package/services/entity-enrichment.ts +43 -2
- package/services/llm-rate-limit-headers.ts +1 -4
- package/services/maintenance-log-analyzer.ts +13 -9
- package/services/reinforcement-extract.ts +19 -0
- package/setup/hybrid-memory-reload-coordinator.ts +26 -0
- package/setup/register-plugin.ts +62 -32
- package/setup/reregister-policy.ts +10 -5
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"embedding-migration.js","names":[],"sources":["../../services/embedding-migration.ts"],"sourcesContent":["/**\n * Embedding migration service: re-generate embeddings when the model or provider changes.\n * Handles LanceDB table swap when vector dimensions differ between old and new model.\n *\n * Issue #153: Migration tooling for switching embedding models.\n */\n\nimport type { FactsDB } from \"../backends/facts-db.js\";\nimport type { VectorDB } from \"../backends/vector-db.js\";\nimport { pluginLogger } from \"../utils/logger.js\";\nimport { is403QuotaOrRateLimitLike, is429OrWrapped } from \"./chat.js\";\nimport type { EmbeddingProvider } from \"./embeddings.js\";\nimport { shouldSuppressEmbeddingError } from \"./embeddings.js\";\nimport { capturePluginError } from \"./error-reporter.js\";\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\nexport interface MigrateEmbeddingsOptions {\n /** SQLite facts backend (source of truth for facts). */\n factsDb: FactsDB;\n /** LanceDB vector backend. Should already be initialized (and auto-repaired if dimensions changed). */\n vectorDb: VectorDB;\n /** The new (target) embedding provider — already configured with the new model. */\n embeddings: EmbeddingProvider;\n /** Number of facts to embed per API call (default: 50). */\n batchSize?: number;\n /**\n * Delay between batches in milliseconds. Spreads load to avoid 429/403 throttling\n * on Azure and other rate-limited providers. Default: 0 (no delay).\n */\n delayMsBetweenBatches?: number;\n /**\n * Progress callback fired after each batch.\n * @param completed Facts processed so far (embedded or skipped).\n * @param total Total facts in SQLite.\n */\n onProgress?: (completed: number, total: number) => void;\n /** Logger for structured output. Falls back to console when not provided. */\n logger?: { info: (msg: string) => void; warn: (msg: string) => void };\n /**\n * Target table name for shadow-table re-indexing. When provided, embeddings are written\n * to this table instead of the main table. Caller is responsible for swapping the shadow\n * table into place after successful migration. Default: undefined (write to main table).\n */\n targetTableName?: string;\n /**\n * Optional checkpoint persistence for resumable re-index.\n * When provided, migration will load the last saved offset and continue from there.\n */\n checkpoint?: {\n load: () => { offset: number } | null;\n save: (state: { offset: number; total: number; migrated: number; skipped: number; ts: number }) => void;\n clear: () => void;\n };\n}\n\ninterface MigrateEmbeddingsResult {\n /** Total facts found in SQLite. */\n total: number;\n /** Facts successfully re-embedded and stored in LanceDB. */\n migrated: number;\n /**\n * Facts skipped (near-duplicate already present, embed returned empty, or\n * a recoverable error occurred). Note: facts that produced an entry in\n * `errors` are also counted here, so `errors.length <= skipped`.\n */\n skipped: number;\n /** Per-fact error strings for failures that were recoverable. */\n errors: string[];\n /**\n * `true` when migration did not reach `total` facts. This includes both\n * VectorDB close mid-run and any other early loop exit (e.g. the underlying\n * data shrank between `getCount` and `getBatch`). Callers MUST treat this\n * as a non-success and surface a partial-state warning.\n */\n aborted: boolean;\n /** Human-readable reason for abort when `aborted` is `true`. */\n abortReason?: string;\n /**\n * Facts handled by the run (`migrated + skipped`). Equals `total` on a\n * fully successful run; less than `total` when `aborted` is `true`.\n */\n processed: number;\n}\n\nexport interface EmbeddingMaintenanceOptions extends MigrateEmbeddingsOptions {\n /** Provider name from the current plugin config (e.g. \"openai\", \"ollama\", \"onnx\"). */\n currentProvider: string;\n /** Model name from the current plugin config (e.g. \"text-embedding-3-small\"). */\n currentModel: string;\n /**\n * When `true`, automatically call `migrateEmbeddings` whenever a config change is detected.\n * When `false` (default), the change is logged but no re-embedding is performed.\n */\n autoMigrate: boolean;\n}\n\ninterface EmbeddingMaintenanceResult {\n /** `true` when the recorded provider/model differs from `currentProvider`/`currentModel`. */\n changed: boolean;\n /** `true` when `migrateEmbeddings` was actually invoked this run. */\n migrated: boolean;\n /** Full migration stats — present only when `migrated` is `true`. */\n result?: MigrateEmbeddingsResult;\n}\n\n// ---------------------------------------------------------------------------\n// Core migration\n// ---------------------------------------------------------------------------\n\n/**\n * Re-generate embeddings for all non-superseded facts using the provided embedding provider.\n *\n * Processing:\n * 1. Loads all non-expired facts from SQLite via `factsDb.getAll()`.\n * 2. Embeds each batch with `embeddings.embedBatch()` (falls back to single-fact embeds on error).\n * 3. Removes any stale LanceDB entry for the fact (handles dimension changes cleanly).\n * 4. Stores the new vector and updates `facts.embedding_model` in SQLite.\n *\n * Aborts early with a warning if the VectorDB is closed mid-run (hot-reload / shutdown).\n * Progress is emitted after every batch via `onProgress` and logged via `logger`.\n *\n * Shadow-table support: When `targetTableName` is provided, vectors are written to that table\n * instead of the main table. This enables non-destructive re-indexing where the shadow table\n * is validated and swapped atomically after successful population.\n */\nexport async function migrateEmbeddings(opts: MigrateEmbeddingsOptions): Promise<MigrateEmbeddingsResult> {\n const {\n factsDb,\n vectorDb,\n embeddings,\n batchSize = 40,\n delayMsBetweenBatches = 0,\n onProgress,\n logger,\n targetTableName,\n } = opts;\n\n const log = logger ?? { info: (m: string) => pluginLogger.info(m), warn: (m: string) => pluginLogger.warn(m) };\n\n const useBatched =\n typeof (factsDb as { getCount?: unknown }).getCount === \"function\" &&\n typeof (factsDb as { getBatch?: unknown }).getBatch === \"function\";\n const total = useBatched\n ? (factsDb as { getCount: (opts: { includeSuperseded: boolean }) => number }).getCount({ includeSuperseded: false })\n : factsDb.getAll({ includeSuperseded: false }).length;\n let migrated = 0;\n let skipped = 0;\n const errors: string[] = [];\n let aborted = false;\n let abortReason: string | undefined;\n\n log.info(\n `memory-hybrid: embedding-migration: starting — ${total} facts, model=${embeddings.modelName}, batchSize=${batchSize}${targetTableName ? `, targetTable=${targetTableName}` : \"\"}`,\n );\n\n let currentGeneration = vectorDb.getCloseGeneration();\n let offset = 0;\n const checkpointState = opts.checkpoint?.load();\n if (\n checkpointState &&\n Number.isFinite(checkpointState.offset) &&\n checkpointState.offset > 0 &&\n checkpointState.offset < total\n ) {\n offset = Math.floor(checkpointState.offset);\n if (!targetTableName) {\n log.warn(\n `memory-hybrid: embedding-migration: ⚠ resuming direct-mode migration from checkpoint offset ${offset}/${total}. Facts before offset ${offset} already have new model vectors; facts from offset ${offset} onward still have old model vectors. The vector table is in a MIXED-MODEL state until migration completes. Semantic search quality is degraded during this window. Re-running with --shadow-table is recommended for large migrations to avoid this split.`,\n );\n } else {\n log.info(`memory-hybrid: embedding-migration: resuming from checkpoint offset ${offset}/${total}`);\n }\n }\n const facts = useBatched ? null : factsDb.getAll({ includeSuperseded: false });\n\n // Helper to store to target table or main table\n const storeVector = async (entry: {\n id: string;\n text: string;\n vector: number[];\n importance: number;\n category: string;\n }) => {\n if (targetTableName) {\n await (vectorDb as { storeToTable?: (name: string, entry: unknown) => Promise<void> }).storeToTable?.(\n targetTableName,\n entry,\n );\n } else {\n await vectorDb.store(entry);\n }\n };\n\n // For shadow-table migration, skip duplicate checks (we're rebuilding from scratch)\n const checkDuplicate = targetTableName ? async () => false : (vec: number[]) => vectorDb.hasDuplicate(vec);\n\n while (offset < total) {\n // VectorDB was recycled mid-run (hot-reload, plugin re-registration, or\n // a stray close from a peer subsystem — see #1248). Try a transparent\n // reconnect first so a routine connection bump no longer guts re-index.\n const newGeneration = vectorDb.getCloseGeneration();\n if (newGeneration !== currentGeneration) {\n log.info(\n `memory-hybrid: embedding-migration: VectorDB closeGeneration advanced (${currentGeneration} → ${newGeneration}) — attempting transparent reconnect at ${migrated + skipped}/${total}...`,\n );\n try {\n await vectorDb.ensureInitialized();\n } catch (err) {\n aborted = true;\n abortReason = `VectorDB reconnect failed: ${err instanceof Error ? err.message : String(err)}`;\n log.warn(\n `memory-hybrid: embedding-migration: reconnect failed at ${migrated + skipped}/${total} — aborting: ${err}`,\n );\n break;\n }\n // `ensureInitialized()` resolves successfully even when LanceDB stays in\n // degraded/FTS-only mode (`lanceInitFailed === true`), in which case\n // `vectorDb.store()` becomes a silent no-op. We MUST verify the LanceDB\n // table is actually writable before claiming the reconnect succeeded.\n if (!vectorDb.isLanceAvailable()) {\n aborted = true;\n abortReason = \"VectorDB reconnected but LanceDB is unavailable (degraded mode)\";\n log.warn(\n `memory-hybrid: embedding-migration: aborted at ${migrated + skipped}/${total} — ${abortReason}; subsequent writes would be silent no-ops`,\n );\n break;\n }\n currentGeneration = vectorDb.getCloseGeneration();\n log.info(\"memory-hybrid: embedding-migration: reconnect successful — continuing migration\");\n }\n\n const batch =\n (useBatched\n ? (\n factsDb as {\n getBatch: (\n offset: number,\n limit: number,\n opts: { includeSuperseded: boolean },\n ) => Array<{ id: string; text: string; importance?: number; category: string }>;\n }\n ).getBatch(offset, batchSize, { includeSuperseded: false })\n : facts?.slice(offset, offset + batchSize)) ?? [];\n if (batch.length === 0) {\n // The loop is supposed to terminate via `offset >= total`. Reaching here\n // with `offset < total` typically means the underlying source returned\n // fewer rows than the snapshot `total` reported — usually because facts\n // expired or were superseded between `getCount()` and `getBatch()` on a\n // long run. This is not a hard failure (the vector store is still\n // consistent for the facts that DO exist), so we log it explicitly as\n // \"ended early\" instead of marking the run as aborted. Callers can\n // safely treat this as a clean termination and update embedding meta.\n if (offset < total) {\n log.warn(\n `memory-hybrid: embedding-migration: ended early at ${migrated + skipped}/${total} ` +\n `— data source drained (${total - offset} facts likely expired or were superseded mid-run)`,\n );\n }\n break;\n }\n const texts = batch.map((f) => f.text);\n\n // Attempt batch embed first; fall back to per-fact embeds on failure\n let vectors: (number[] | null)[];\n try {\n const batchResult = await embeddings.embedBatch(texts);\n vectors = batchResult;\n } catch (batchErr) {\n if (!shouldSuppressEmbeddingError(batchErr)) {\n capturePluginError(batchErr instanceof Error ? batchErr : new Error(String(batchErr)), {\n subsystem: \"embeddings\",\n operation: \"migration-embed-batch\",\n });\n }\n const batchAsErr = batchErr instanceof Error ? batchErr : new Error(String(batchErr));\n const isRateLimit = is429OrWrapped(batchAsErr) || is403QuotaOrRateLimitLike(batchErr);\n\n if (isRateLimit) {\n // Back off before per-fact fallback to avoid amplifying RPM pressure (#940 §3).\n const backoffMs = delayMsBetweenBatches > 0 ? delayMsBetweenBatches * 3 : 5_000;\n log.warn(\n `[embedding-quota] memory-hybrid: embedding-migration: batch rate-limited at offset ${offset} — backing off ${backoffMs}ms before per-fact fallback`,\n );\n await new Promise((r) => setTimeout(r, backoffMs));\n } else {\n log.warn(\n `memory-hybrid: embedding-migration: batch embed failed at offset ${offset} — falling back to per-fact embeds: ${batchErr}`,\n );\n }\n\n // Per-fact fallback: sequential with delay when rate-limited (prevents RPM storm).\n vectors = [];\n for (const fact of batch) {\n try {\n vectors.push(await embeddings.embed(fact.text));\n } catch (err) {\n if (!shouldSuppressEmbeddingError(err)) {\n capturePluginError(err instanceof Error ? err : new Error(String(err)), {\n subsystem: \"embeddings\",\n operation: \"migration-embed-single\",\n });\n }\n const singleAsErr = err instanceof Error ? err : new Error(String(err));\n if (is429OrWrapped(singleAsErr) || is403QuotaOrRateLimitLike(err)) {\n const singleBackoff = delayMsBetweenBatches > 0 ? delayMsBetweenBatches * 2 : 3_000;\n log.warn(\n `[embedding-quota] memory-hybrid: embedding-migration: per-fact rate-limited for ${fact.id} — backing off ${singleBackoff}ms`,\n );\n await new Promise((r) => setTimeout(r, singleBackoff));\n }\n errors.push(`fact ${fact.id}: embed failed — ${String(err)}`);\n vectors.push(null);\n }\n if (isRateLimit && delayMsBetweenBatches > 0) {\n await new Promise((r) => setTimeout(r, delayMsBetweenBatches));\n }\n }\n }\n\n for (let j = 0; j < batch.length; j++) {\n const fact = batch[j];\n const vec = vectors[j];\n if (!fact) continue;\n\n if (vec === null || !vec || vec.length === 0) {\n skipped++;\n continue;\n }\n\n try {\n // Check for duplicate BEFORE deleting old vector: if a different fact already has\n // a similar embedding, skip rather than removing the existing entry unnecessarily.\n const isDuplicate = await checkDuplicate(vec);\n if (!isDuplicate) {\n // Remove stale entry so dimension-changed stores succeed (only for main table)\n if (!targetTableName) {\n try {\n await vectorDb.delete(fact.id);\n } catch {\n // Entry may not exist — expected on first migration\n }\n }\n await storeVector({\n id: fact.id,\n text: fact.text,\n vector: vec,\n importance: fact.importance ?? 0.5,\n category: fact.category,\n });\n // Update SQLite metadata (only when writing to main table, not shadow)\n if (!targetTableName) {\n factsDb.setEmbeddingModel(fact.id, embeddings.modelName);\n }\n migrated++;\n } else {\n skipped++;\n }\n } catch (err) {\n capturePluginError(err instanceof Error ? err : new Error(String(err)), {\n subsystem: \"vector\",\n operation: \"migration-store\",\n });\n errors.push(`fact ${fact.id}: store failed — ${String(err)}`);\n skipped++;\n }\n }\n\n offset += batch.length;\n try {\n opts.checkpoint?.save({\n offset,\n total,\n migrated,\n skipped,\n ts: Date.now(),\n });\n } catch (err) {\n log.warn(`memory-hybrid: embedding-migration: checkpoint save failed (non-fatal): ${err}`);\n }\n onProgress?.(offset, total);\n\n // Periodic progress log for large datasets\n if (total >= 100 && offset % 100 < batch.length) {\n log.info(\n `memory-hybrid: embedding-migration: ${offset}/${total} processed ` +\n `(${migrated} migrated, ${errors.length} errors)`,\n );\n }\n\n // Inter-batch throttle to stay under provider rate limits (Azure/OpenAI).\n if (delayMsBetweenBatches > 0 && offset < total) {\n await new Promise((r) => setTimeout(r, delayMsBetweenBatches));\n }\n }\n\n const processed = migrated + skipped;\n\n if (aborted) {\n log.warn(\n `memory-hybrid: embedding-migration: ABORTED at ${processed}/${total} — ${abortReason}; ${total - processed} facts not re-embedded`,\n );\n } else {\n log.info(\n `memory-hybrid: embedding-migration: complete — ${migrated} migrated, ${skipped} skipped, ${errors.length} errors (total ${total})`,\n );\n opts.checkpoint?.clear();\n }\n\n return { total, migrated, skipped, errors, aborted, abortReason, processed };\n}\n\n// ---------------------------------------------------------------------------\n// Maintenance: detect config change → optionally migrate\n// ---------------------------------------------------------------------------\n\n/**\n * Maintenance function: compare the current embedding config against the last recorded\n * config (stored in the SQLite `embedding_meta` table). When a change is detected and\n * `autoMigrate` is `true`, triggers a full re-embedding run via `migrateEmbeddings`.\n *\n * On first run (no previous meta), the current config is recorded and no migration occurs.\n * On config change with `autoMigrate=true`, the meta is updated only after successful migration,\n * ensuring failed migrations will retry on the next restart.\n *\n * @returns `changed` — whether provider or model differed from the recorded values.\n * `migrated` — whether re-embedding was actually performed.\n * `result` — migration stats when `migrated` is `true`.\n */\nexport async function runEmbeddingMaintenance(opts: EmbeddingMaintenanceOptions): Promise<EmbeddingMaintenanceResult> {\n const { factsDb, currentProvider, currentModel, autoMigrate, logger } = opts;\n const log = logger ?? { info: (m: string) => pluginLogger.info(m), warn: (m: string) => pluginLogger.warn(m) };\n\n let changed = false;\n let previousMeta;\n\n try {\n previousMeta = factsDb.getEmbeddingMeta();\n\n if (previousMeta) {\n changed = previousMeta.provider !== currentProvider || previousMeta.model !== currentModel;\n } else {\n // First run — record initial state and return early\n factsDb.setEmbeddingMeta(currentProvider, currentModel);\n return { changed: false, migrated: false };\n }\n } catch (err) {\n log.warn(`memory-hybrid: embedding-migration: failed to read/write embedding metadata (non-fatal): ${err}`);\n // Cannot determine whether change happened — skip migration to be safe\n return { changed: false, migrated: false };\n }\n\n if (!changed) {\n return { changed: false, migrated: false };\n }\n\n log.info(`memory-hybrid: embedding config changed — now using provider=${currentProvider}, model=${currentModel}`);\n\n if (!autoMigrate) {\n log.warn(\n \"memory-hybrid: embedding.autoMigrate=false — skipping automatic re-embedding. \" +\n \"Set embedding.autoMigrate=true in plugin config to automatically re-generate \" +\n \"embeddings when the model changes, or run the maintenance command manually.\",\n );\n factsDb.setEmbeddingMeta(currentProvider, currentModel);\n return { changed: true, migrated: false };\n }\n\n log.info(\"memory-hybrid: embedding.autoMigrate=true — starting re-embedding of existing facts...\");\n\n try {\n const result = await migrateEmbeddings(opts);\n // Only update meta after a fully successful (non-aborted) migration so the\n // next restart will retry. Still surface that the migration was attempted\n // (`migrated: true`) and propagate `result` so callers can inspect\n // `result.aborted` and the partial counts.\n if (result.aborted) {\n log.warn(\n `memory-hybrid: embedding-migration: maintenance run aborted — meta not updated. ${result.migrated} embedded, ${result.skipped} skipped, ${result.errors.length} errors (processed ${result.processed}/${result.total}). Re-run maintenance or re-index to complete.`,\n );\n return { changed: true, migrated: true, result };\n }\n factsDb.setEmbeddingMeta(currentProvider, currentModel);\n return { changed: true, migrated: true, result };\n } catch (err) {\n capturePluginError(err instanceof Error ? err : new Error(String(err)), {\n subsystem: \"embeddings\",\n operation: \"runEmbeddingMaintenance\",\n });\n log.warn(`memory-hybrid: embedding-migration: maintenance run failed: ${err}`);\n return { changed: true, migrated: false };\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"embedding-migration.js","names":[],"sources":["../../services/embedding-migration.ts"],"sourcesContent":["/**\n * Embedding migration service: re-generate embeddings when the model or provider changes.\n * Handles LanceDB table swap when vector dimensions differ between old and new model.\n *\n * Issue #153: Migration tooling for switching embedding models.\n */\n\nimport type { FactsDB } from \"../backends/facts-db.js\";\nimport type { VectorDB } from \"../backends/vector-db.js\";\nimport { pluginLogger } from \"../utils/logger.js\";\nimport { is403QuotaOrRateLimitLike, is429OrWrapped } from \"./chat.js\";\nimport type { EmbeddingProvider } from \"./embeddings.js\";\nimport { shouldSuppressEmbeddingError } from \"./embeddings.js\";\nimport { capturePluginError } from \"./error-reporter.js\";\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\nexport interface MigrateEmbeddingsOptions {\n /** SQLite facts backend (source of truth for facts). */\n factsDb: FactsDB;\n /** LanceDB vector backend. Should already be initialized (and auto-repaired if dimensions changed). */\n vectorDb: VectorDB;\n /** The new (target) embedding provider — already configured with the new model. */\n embeddings: EmbeddingProvider;\n /** Number of facts to embed per API call (default: 50). */\n batchSize?: number;\n /**\n * Delay between batches in milliseconds. Spreads load to avoid 429/403 throttling\n * on Azure and other rate-limited providers. Default: 0 (no delay).\n */\n delayMsBetweenBatches?: number;\n /**\n * Progress callback fired after each batch.\n * @param completed Facts processed so far (embedded or skipped).\n * @param total Total facts in SQLite.\n */\n onProgress?: (completed: number, total: number) => void;\n /** Logger for structured output. Falls back to console when not provided. */\n logger?: { info: (msg: string) => void; warn: (msg: string) => void };\n /**\n * Target table name for shadow-table re-indexing. When provided, embeddings are written\n * to this table instead of the main table. Caller is responsible for swapping the shadow\n * table into place after successful migration. Default: undefined (write to main table).\n */\n targetTableName?: string;\n /**\n * Optional checkpoint persistence for resumable re-index.\n * When provided, migration will load the last saved offset and continue from there.\n */\n checkpoint?: {\n load: () => { offset: number } | null;\n save: (state: { offset: number; total: number; migrated: number; skipped: number; ts: number }) => void;\n clear: () => void;\n };\n}\n\ninterface MigrateEmbeddingsResult {\n /** Total facts found in SQLite. */\n total: number;\n /** Facts successfully re-embedded and stored in LanceDB. */\n migrated: number;\n /**\n * Facts skipped (near-duplicate already present, embed returned empty, or\n * a recoverable error occurred). Note: facts that produced an entry in\n * `errors` are also counted here, so `errors.length <= skipped`.\n */\n skipped: number;\n /** Per-fact error strings for failures that were recoverable. */\n errors: string[];\n /**\n * `true` when migration did not reach `total` facts. This includes both\n * VectorDB close mid-run and any other early loop exit (e.g. the underlying\n * data shrank between `getCount` and `getBatch`). Callers MUST treat this\n * as a non-success and surface a partial-state warning.\n */\n aborted: boolean;\n /** Human-readable reason for abort when `aborted` is `true`. */\n abortReason?: string;\n /**\n * Facts handled by the run (`migrated + skipped`). Equals `total` on a\n * fully successful run; less than `total` when `aborted` is `true`.\n */\n processed: number;\n}\n\nexport interface EmbeddingMaintenanceOptions extends MigrateEmbeddingsOptions {\n /** Provider name from the current plugin config (e.g. \"openai\", \"ollama\", \"onnx\"). */\n currentProvider: string;\n /** Model name from the current plugin config (e.g. \"text-embedding-3-small\"). */\n currentModel: string;\n /**\n * When `true`, automatically call `migrateEmbeddings` whenever a config change is detected.\n * When `false` (default), the change is logged but no re-embedding is performed.\n */\n autoMigrate: boolean;\n}\n\ninterface EmbeddingMaintenanceResult {\n /** `true` when the recorded provider/model differs from `currentProvider`/`currentModel`. */\n changed: boolean;\n /** `true` when `migrateEmbeddings` was actually invoked this run. */\n migrated: boolean;\n /** Full migration stats — present only when `migrated` is `true`. */\n result?: MigrateEmbeddingsResult;\n}\n\n// ---------------------------------------------------------------------------\n// Core migration\n// ---------------------------------------------------------------------------\n\n/**\n * Re-generate embeddings for all non-superseded facts using the provided embedding provider.\n *\n * Processing:\n * 1. Loads all non-expired facts from SQLite via `factsDb.getAll()`.\n * 2. Embeds each batch with `embeddings.embedBatch()` (falls back to single-fact embeds on error).\n * 3. Removes any stale LanceDB entry for the fact (handles dimension changes cleanly).\n * 4. Stores the new vector and updates `facts.embedding_model` in SQLite.\n *\n * Aborts early with a warning if the VectorDB is closed mid-run (hot-reload / shutdown).\n * Progress is emitted after every batch via `onProgress` and logged via `logger`.\n *\n * Shadow-table support: When `targetTableName` is provided, vectors are written to that table\n * instead of the main table. This enables non-destructive re-indexing where the shadow table\n * is validated and swapped atomically after successful population.\n */\nexport async function migrateEmbeddings(opts: MigrateEmbeddingsOptions): Promise<MigrateEmbeddingsResult> {\n const {\n factsDb,\n vectorDb,\n embeddings,\n batchSize = 40,\n delayMsBetweenBatches = 0,\n onProgress,\n logger,\n targetTableName,\n } = opts;\n\n const log = logger ?? { info: (m: string) => pluginLogger.info(m), warn: (m: string) => pluginLogger.warn(m) };\n\n const useBatched =\n typeof (factsDb as { getCount?: unknown }).getCount === \"function\" &&\n typeof (factsDb as { getBatch?: unknown }).getBatch === \"function\";\n const total = useBatched\n ? (factsDb as { getCount: (opts: { includeSuperseded: boolean }) => number }).getCount({ includeSuperseded: false })\n : factsDb.getAll({ includeSuperseded: false }).length;\n let migrated = 0;\n let skipped = 0;\n const errors: string[] = [];\n let aborted = false;\n let abortReason: string | undefined;\n\n log.info(\n `memory-hybrid: embedding-migration: starting — ${total} facts, model=${embeddings.modelName}, batchSize=${batchSize}${targetTableName ? `, targetTable=${targetTableName}` : \"\"}`,\n );\n\n let currentGeneration = vectorDb.getCloseGeneration();\n let offset = 0;\n const checkpointState = opts.checkpoint?.load();\n if (\n checkpointState &&\n Number.isFinite(checkpointState.offset) &&\n checkpointState.offset > 0 &&\n checkpointState.offset < total\n ) {\n offset = Math.floor(checkpointState.offset);\n if (!targetTableName) {\n log.warn(\n `memory-hybrid: embedding-migration: ⚠ resuming direct-mode migration from checkpoint offset ${offset}/${total}. Facts before offset ${offset} already have new model vectors; facts from offset ${offset} onward still have old model vectors. The vector table is in a MIXED-MODEL state until migration completes. Semantic search quality is degraded during this window. Re-running with --shadow-table is recommended for large migrations to avoid this split.`,\n );\n } else {\n log.info(`memory-hybrid: embedding-migration: resuming from checkpoint offset ${offset}/${total}`);\n }\n }\n const facts = useBatched ? null : factsDb.getAll({ includeSuperseded: false });\n\n // Helper to store to target table or main table\n const storeVector = async (entry: {\n id: string;\n text: string;\n vector: number[];\n importance: number;\n category: string;\n }) => {\n if (targetTableName) {\n await (vectorDb as { storeToTable?: (name: string, entry: unknown) => Promise<void> }).storeToTable?.(\n targetTableName,\n entry,\n );\n } else {\n await vectorDb.store(entry);\n }\n };\n\n // For shadow-table migration, skip duplicate checks (we're rebuilding from scratch)\n const checkDuplicate = targetTableName ? async () => false : (vec: number[]) => vectorDb.hasDuplicate(vec);\n\n while (offset < total) {\n // VectorDB was recycled mid-run (hot-reload, plugin re-registration, or\n // a stray close from a peer subsystem — see #1248). Try a transparent\n // reconnect first so a routine connection bump no longer guts re-index.\n const newGeneration = vectorDb.getCloseGeneration();\n if (newGeneration !== currentGeneration) {\n log.info(\n `memory-hybrid: embedding-migration: VectorDB closeGeneration advanced (${currentGeneration} → ${newGeneration}) — attempting transparent reconnect at ${migrated + skipped}/${total}...`,\n );\n try {\n await vectorDb.ensureInitialized();\n } catch (err) {\n aborted = true;\n abortReason = `VectorDB reconnect failed: ${err instanceof Error ? err.message : String(err)}`;\n log.warn(\n `memory-hybrid: embedding-migration: reconnect failed at ${migrated + skipped}/${total} — aborting: ${err}`,\n );\n break;\n }\n // `ensureInitialized()` resolves successfully even when LanceDB stays in\n // degraded/FTS-only mode (`lanceInitFailed === true`), in which case\n // `vectorDb.store()` becomes a silent no-op. We MUST verify the LanceDB\n // table is actually writable before claiming the reconnect succeeded.\n if (!vectorDb.isLanceAvailable()) {\n aborted = true;\n abortReason = \"VectorDB reconnected but LanceDB is unavailable (degraded mode)\";\n log.warn(\n `memory-hybrid: embedding-migration: aborted at ${migrated + skipped}/${total} — ${abortReason}; subsequent writes would be silent no-ops`,\n );\n break;\n }\n currentGeneration = vectorDb.getCloseGeneration();\n log.info(\"memory-hybrid: embedding-migration: reconnect successful — continuing migration\");\n }\n\n const batch =\n (useBatched\n ? (\n factsDb as {\n getBatch: (\n offset: number,\n limit: number,\n opts: { includeSuperseded: boolean },\n ) => Array<{ id: string; text: string; importance?: number; category: string }>;\n }\n ).getBatch(offset, batchSize, { includeSuperseded: false })\n : facts?.slice(offset, offset + batchSize)) ?? [];\n if (batch.length === 0) {\n // The loop is supposed to terminate via `offset >= total`. Reaching here\n // with `offset < total` typically means the underlying source returned\n // fewer rows than the snapshot `total` reported — usually because facts\n // expired or were superseded between `getCount()` and `getBatch()` on a\n // long run. This is not a hard failure (the vector store is still\n // consistent for the facts that DO exist), so we log it explicitly as\n // \"ended early\" instead of marking the run as aborted. Callers can\n // safely treat this as a clean termination and update embedding meta.\n if (offset < total) {\n log.warn(\n `memory-hybrid: embedding-migration: ended early at ${migrated + skipped}/${total} ` +\n `— data source drained (${total - offset} facts likely expired or were superseded mid-run)`,\n );\n }\n break;\n }\n const texts = batch.map((f) => f.text);\n\n // Attempt batch embed first; fall back to per-fact embeds on failure\n let vectors: (number[] | null)[];\n try {\n const batchResult = await embeddings.embedBatch(texts);\n vectors = batchResult;\n } catch (batchErr) {\n if (!shouldSuppressEmbeddingError(batchErr)) {\n capturePluginError(batchErr instanceof Error ? batchErr : new Error(String(batchErr)), {\n subsystem: \"embeddings\",\n operation: \"migration-embed-batch\",\n });\n }\n const batchAsErr = batchErr instanceof Error ? batchErr : new Error(String(batchErr));\n const isRateLimit = is429OrWrapped(batchAsErr) || is403QuotaOrRateLimitLike(batchErr);\n\n if (isRateLimit) {\n // Back off before per-fact fallback to avoid amplifying RPM pressure (#940 §3).\n const backoffMs = delayMsBetweenBatches > 0 ? delayMsBetweenBatches * 3 : 5_000;\n log.warn(\n `[embedding-quota] memory-hybrid: embedding-migration: batch rate-limited at offset ${offset} — backing off ${backoffMs}ms before per-fact fallback`,\n );\n await new Promise((r) => setTimeout(r, backoffMs));\n } else {\n log.warn(\n `memory-hybrid: embedding-migration: batch embed failed at offset ${offset} — falling back to per-fact embeds: ${batchErr}`,\n );\n }\n\n // Per-fact fallback: sequential with delay when rate-limited (prevents RPM storm).\n vectors = [];\n for (const fact of batch) {\n try {\n vectors.push(await embeddings.embed(fact.text));\n } catch (err) {\n if (!shouldSuppressEmbeddingError(err)) {\n capturePluginError(err instanceof Error ? err : new Error(String(err)), {\n subsystem: \"embeddings\",\n operation: \"migration-embed-single\",\n });\n }\n const singleAsErr = err instanceof Error ? err : new Error(String(err));\n if (is429OrWrapped(singleAsErr) || is403QuotaOrRateLimitLike(err)) {\n const singleBackoff = delayMsBetweenBatches > 0 ? delayMsBetweenBatches * 2 : 3_000;\n log.warn(\n `[embedding-quota] memory-hybrid: embedding-migration: per-fact rate-limited for ${fact.id} — backing off ${singleBackoff}ms`,\n );\n await new Promise((r) => setTimeout(r, singleBackoff));\n }\n errors.push(`fact ${fact.id}: embed failed — ${String(err)}`);\n vectors.push(null);\n }\n if (isRateLimit && delayMsBetweenBatches > 0) {\n await new Promise((r) => setTimeout(r, delayMsBetweenBatches));\n }\n }\n }\n\n for (let j = 0; j < batch.length; j++) {\n const fact = batch[j];\n const vec = vectors[j];\n if (!fact) continue;\n\n if (vec === null || !vec || vec.length === 0) {\n skipped++;\n continue;\n }\n\n try {\n // Check for duplicate BEFORE deleting old vector: if a different fact already has\n // a similar embedding, skip rather than removing the existing entry unnecessarily.\n const isDuplicate = await checkDuplicate(vec);\n if (!isDuplicate) {\n // Remove stale entry so dimension-changed stores succeed (only for main table)\n if (!targetTableName) {\n try {\n await vectorDb.delete(fact.id);\n } catch {\n // Entry may not exist — expected on first migration\n }\n }\n await storeVector({\n id: fact.id,\n text: fact.text,\n vector: vec,\n importance: fact.importance ?? 0.5,\n category: fact.category,\n });\n // Update SQLite metadata (only when writing to main table, not shadow)\n if (!targetTableName) {\n factsDb.setEmbeddingModel(fact.id, embeddings.modelName);\n }\n migrated++;\n } else {\n skipped++;\n }\n } catch (err) {\n capturePluginError(err instanceof Error ? err : new Error(String(err)), {\n subsystem: \"vector\",\n operation: \"migration-store\",\n });\n errors.push(`fact ${fact.id}: store failed — ${String(err)}`);\n skipped++;\n }\n }\n\n offset += batch.length;\n try {\n opts.checkpoint?.save({\n offset,\n total,\n migrated,\n skipped,\n ts: Date.now(),\n });\n } catch (err) {\n log.warn(`memory-hybrid: embedding-migration: checkpoint save failed (non-fatal): ${err}`);\n }\n onProgress?.(offset, total);\n\n // Periodic progress log for large datasets\n if (total >= 100 && offset % 100 < batch.length) {\n log.info(\n `memory-hybrid: embedding-migration: ${offset}/${total} processed ` +\n `(${migrated} migrated, ${errors.length} errors)`,\n );\n }\n\n // Inter-batch throttle to stay under provider rate limits (Azure/OpenAI).\n if (delayMsBetweenBatches > 0 && offset < total) {\n await new Promise((r) => setTimeout(r, delayMsBetweenBatches));\n }\n }\n\n const processed = migrated + skipped;\n\n if (aborted) {\n log.warn(\n `memory-hybrid: embedding-migration: ABORTED at ${processed}/${total} — ${abortReason}; ${total - processed} facts not re-embedded`,\n );\n } else {\n log.info(\n `memory-hybrid: embedding-migration: complete — ${migrated} migrated, ${skipped} skipped, ${errors.length} errors (total ${total})`,\n );\n opts.checkpoint?.clear();\n }\n\n return { total, migrated, skipped, errors, aborted, abortReason, processed };\n}\n\n// ---------------------------------------------------------------------------\n// Maintenance: detect config change → optionally migrate\n// ---------------------------------------------------------------------------\n\n/**\n * Maintenance function: compare the current embedding config against the last recorded\n * config (stored in the SQLite `embedding_meta` table). When a change is detected and\n * `autoMigrate` is `true`, triggers a full re-embedding run via `migrateEmbeddings`.\n *\n * On first run (no previous meta), the current config is recorded and no migration occurs.\n * On config change with `autoMigrate=true`, the meta is updated only after successful migration,\n * ensuring failed migrations will retry on the next restart.\n *\n * @returns `changed` — whether provider or model differed from the recorded values.\n * `migrated` — whether re-embedding was actually performed.\n * `result` — migration stats when `migrated` is `true`.\n */\nexport async function runEmbeddingMaintenance(opts: EmbeddingMaintenanceOptions): Promise<EmbeddingMaintenanceResult> {\n const { factsDb, currentProvider, currentModel, autoMigrate, logger } = opts;\n const log = logger ?? { info: (m: string) => pluginLogger.info(m), warn: (m: string) => pluginLogger.warn(m) };\n\n let changed = false;\n let previousMeta;\n\n try {\n previousMeta = factsDb.getEmbeddingMeta();\n\n if (previousMeta) {\n changed = previousMeta.provider !== currentProvider || previousMeta.model !== currentModel;\n } else {\n // First run — record initial state and return early\n factsDb.setEmbeddingMeta(currentProvider, currentModel);\n return { changed: false, migrated: false };\n }\n } catch (err) {\n log.warn(`memory-hybrid: embedding-migration: failed to read/write embedding metadata (non-fatal): ${err}`);\n // Cannot determine whether change happened — skip migration to be safe\n return { changed: false, migrated: false };\n }\n\n if (!changed) {\n return { changed: false, migrated: false };\n }\n\n log.info(`memory-hybrid: embedding config changed — now using provider=${currentProvider}, model=${currentModel}`);\n\n if (!autoMigrate) {\n log.warn(\n \"memory-hybrid: embedding.autoMigrate=false — skipping automatic re-embedding. \" +\n \"Set embedding.autoMigrate=true in plugin config to automatically re-generate \" +\n \"embeddings when the model changes, or run the maintenance command manually.\",\n );\n factsDb.setEmbeddingMeta(currentProvider, currentModel);\n return { changed: true, migrated: false };\n }\n\n log.info(\"memory-hybrid: embedding.autoMigrate=true — starting re-embedding of existing facts...\");\n\n try {\n const result = await migrateEmbeddings(opts);\n // Only update meta after a fully successful (non-aborted) migration so the\n // next restart will retry. Still surface that the migration was attempted\n // (`migrated: true`) and propagate `result` so callers can inspect\n // `result.aborted` and the partial counts.\n if (result.aborted) {\n log.warn(\n `memory-hybrid: embedding-migration: maintenance run aborted — meta not updated. ${result.migrated} embedded, ${result.skipped} skipped, ${result.errors.length} errors (processed ${result.processed}/${result.total}). Re-run maintenance or re-index to complete.`,\n );\n return { changed: true, migrated: true, result };\n }\n factsDb.setEmbeddingMeta(currentProvider, currentModel);\n return { changed: true, migrated: true, result };\n } catch (err) {\n capturePluginError(err instanceof Error ? err : new Error(String(err)), {\n subsystem: \"embeddings\",\n operation: \"runEmbeddingMaintenance\",\n });\n log.warn(`memory-hybrid: embedding-migration: maintenance run failed: ${err}`);\n return { changed: true, migrated: false };\n }\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"embedding-registry.js","names":[],"sources":["../../services/embedding-registry.ts"],"sourcesContent":["/**\n * EmbeddingRegistry: manages multiple embedding models per fact (Issue #158).\n *\n * Different models capture different semantic angles (general, domain-specific,\n * query-tuned). The registry creates providers lazily and exposes a unified API\n * for embedding text with one or all registered models.\n *\n * If no additional models are registered (empty/undefined multiModels config),\n * the system works exactly as before — single-model mode with no performance impact.\n */\n\nimport OpenAI from \"openai\";\nimport type { EmbeddingModelConfig } from \"../config.js\";\nimport { type EmbeddingProvider, Embeddings, OllamaEmbeddingProvider, OnnxEmbeddingProvider } from \"./embeddings.js\";\nimport { capturePluginError } from \"./error-reporter.js\";\n\n// ---------------------------------------------------------------------------\n// EmbeddingRegistry\n// ---------------------------------------------------------------------------\n\n/**\n * Registry for multiple embedding models.\n *\n * Usage:\n * ```ts\n * const registry = new EmbeddingRegistry(primaryProvider, primaryModelName);\n * registry.register({ name: \"nomic-embed-text\", provider: \"ollama\", dimensions: 768, role: \"domain\" });\n *\n * // Embed with all models\n * const vectors = await registry.embedAll(\"some text\");\n * // => Map { \"primary\" => Float32Array, \"nomic-embed-text\" => Float32Array }\n *\n * // Embed with a specific model\n * const vec = await registry.embed(\"some text\", \"nomic-embed-text\");\n * ```\n */\nexport class EmbeddingRegistry {\n /** The primary (existing) embedding provider. */\n private readonly primary: EmbeddingProvider;\n /** Canonical name for the primary model (used as map key). */\n private readonly primaryName: string;\n\n /** Additional model configs (populated via register()). */\n private readonly modelConfigs: EmbeddingModelConfig[] = [];\n /** Lazily-initialized providers for additional models. */\n private readonly providers = new Map<string, EmbeddingProvider>();\n\n constructor(primary: EmbeddingProvider, primaryName: string) {\n this.primary = primary;\n this.primaryName = primaryName;\n }\n\n /**\n * Register an additional embedding model.\n * Skips models that are disabled (enabled === false).\n * Skips registration if a model with the same name already exists.\n */\n register(config: EmbeddingModelConfig): void {\n if (config.enabled === false) return;\n // Prevent duplicate registrations\n if (config.name === this.primaryName) return;\n if (this.modelConfigs.some((m) => m.name === config.name)) return;\n this.modelConfigs.push(config);\n }\n\n /**\n * Get all registered model configs (additional models only; does not include primary).\n */\n getModels(): EmbeddingModelConfig[] {\n return [...this.modelConfigs];\n }\n\n /**\n * Get the primary model config-like object.\n */\n getPrimaryModel(): { name: string; provider: string; dimensions: number } {\n return {\n name: this.primaryName,\n provider: this.primary.constructor.name,\n dimensions: this.primary.dimensions,\n };\n }\n\n /** Whether any additional models are registered (multi-model mode). */\n isMultiModel(): boolean {\n return this.modelConfigs.length > 0;\n }\n\n /**\n * Embed text with a specific model by name.\n * Pass undefined or primaryName to use the primary model.\n */\n async embed(text: string, modelName?: string): Promise<Float32Array> {\n if (!modelName || modelName === this.primaryName) {\n const vec = await this.primary.embed(text);\n return toFloat32Array(vec);\n }\n const provider = this.getOrCreateProvider(modelName);\n const vec = await provider.embed(text);\n return toFloat32Array(vec);\n }\n\n /**\n * Embed text with ALL registered models (primary + additional).\n * Returns a Map from model name → Float32Array.\n * On partial failure, reports each error via capturePluginError and returns\n * whatever succeeded so callers get partial results instead of losing all data.\n */\n async embedAll(text: string): Promise<Map<string, Float32Array>> {\n const result = new Map<string, Float32Array>();\n\n // Primary model first\n const primaryVec = await this.primary.embed(text);\n result.set(this.primaryName, toFloat32Array(primaryVec));\n\n // Additional models in parallel\n if (this.modelConfigs.length > 0) {\n const tasks = this.modelConfigs.map(async (cfg) => {\n const provider = this.getOrCreateProvider(cfg.name);\n const vec = await provider.embed(text);\n return [cfg.name, toFloat32Array(vec)] as const;\n });\n const settled = await Promise.allSettled(tasks);\n for (const s of settled) {\n if (s.status === \"fulfilled\") {\n const [name, vec] = s.value;\n result.set(name, vec);\n } else {\n capturePluginError(s.reason instanceof Error ? s.reason : new Error(String(s.reason)), {\n subsystem: \"embedding-registry\",\n operation: \"embedAll\",\n });\n }\n }\n }\n\n return result;\n }\n\n /**\n * Return the names of all models (primary + additional enabled models).\n */\n allModelNames(): string[] {\n return [this.primaryName, ...this.modelConfigs.map((m) => m.name)];\n }\n\n // ---------------------------------------------------------------------------\n // Internal: lazy provider creation\n // ---------------------------------------------------------------------------\n\n private getOrCreateProvider(modelName: string): EmbeddingProvider {\n const existing = this.providers.get(modelName);\n if (existing) return existing;\n\n const cfg = this.modelConfigs.find((m) => m.name === modelName);\n if (!cfg) {\n throw new Error(`No embedding model registered with name '${modelName}'`);\n }\n\n const provider = createProviderForConfig(cfg);\n this.providers.set(modelName, provider);\n return provider;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Helper: create an EmbeddingProvider from an EmbeddingModelConfig\n// ---------------------------------------------------------------------------\n\nfunction createProviderForConfig(cfg: EmbeddingModelConfig): EmbeddingProvider {\n if (cfg.provider === \"ollama\") {\n return new OllamaEmbeddingProvider({\n model: cfg.name,\n dimensions: cfg.dimensions,\n endpoint: cfg.endpoint,\n });\n }\n\n if (cfg.provider === \"openai\") {\n if (!cfg.apiKey) {\n throw new Error(`EmbeddingModelConfig for '${cfg.name}': apiKey is required for openai provider`);\n }\n const client = new OpenAI({ apiKey: cfg.apiKey });\n return new Embeddings(client, cfg.name, cfg.dimensions);\n }\n\n if (cfg.provider === \"onnx\") {\n return new OnnxEmbeddingProvider({\n model: cfg.name,\n dimensions: cfg.dimensions,\n });\n }\n\n throw new Error(`EmbeddingModelConfig for '${cfg.name}': unknown provider '${cfg.provider as string}'`);\n}\n\n// ---------------------------------------------------------------------------\n// Utility\n// ---------------------------------------------------------------------------\n\n/** Convert number[] or Float32Array to Float32Array. */\nexport function toFloat32Array(vec: number[] | Float32Array): Float32Array {\n return vec instanceof Float32Array ? vec : new Float32Array(vec);\n}\n\n/**\n * Build an EmbeddingRegistry from plugin config.\n * If cfg.multiModels is empty/undefined, returns a single-model registry.\n */\nexport function buildEmbeddingRegistry(\n primary: EmbeddingProvider,\n primaryName: string,\n multiModels?: EmbeddingModelConfig[],\n): EmbeddingRegistry {\n const registry = new EmbeddingRegistry(primary, primaryName);\n if (multiModels && multiModels.length > 0) {\n for (const modelCfg of multiModels) {\n registry.register(modelCfg);\n }\n }\n return registry;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"embedding-registry.js","names":[],"sources":["../../services/embedding-registry.ts"],"sourcesContent":["/**\n * EmbeddingRegistry: manages multiple embedding models per fact (Issue #158).\n *\n * Different models capture different semantic angles (general, domain-specific,\n * query-tuned). The registry creates providers lazily and exposes a unified API\n * for embedding text with one or all registered models.\n *\n * If no additional models are registered (empty/undefined multiModels config),\n * the system works exactly as before — single-model mode with no performance impact.\n */\n\nimport OpenAI from \"openai\";\nimport type { EmbeddingModelConfig } from \"../config.js\";\nimport { type EmbeddingProvider, Embeddings, OllamaEmbeddingProvider, OnnxEmbeddingProvider } from \"./embeddings.js\";\nimport { capturePluginError } from \"./error-reporter.js\";\n\n// ---------------------------------------------------------------------------\n// EmbeddingRegistry\n// ---------------------------------------------------------------------------\n\n/**\n * Registry for multiple embedding models.\n *\n * Usage:\n * ```ts\n * const registry = new EmbeddingRegistry(primaryProvider, primaryModelName);\n * registry.register({ name: \"nomic-embed-text\", provider: \"ollama\", dimensions: 768, role: \"domain\" });\n *\n * // Embed with all models\n * const vectors = await registry.embedAll(\"some text\");\n * // => Map { \"primary\" => Float32Array, \"nomic-embed-text\" => Float32Array }\n *\n * // Embed with a specific model\n * const vec = await registry.embed(\"some text\", \"nomic-embed-text\");\n * ```\n */\nexport class EmbeddingRegistry {\n /** The primary (existing) embedding provider. */\n private readonly primary: EmbeddingProvider;\n /** Canonical name for the primary model (used as map key). */\n private readonly primaryName: string;\n\n /** Additional model configs (populated via register()). */\n private readonly modelConfigs: EmbeddingModelConfig[] = [];\n /** Lazily-initialized providers for additional models. */\n private readonly providers = new Map<string, EmbeddingProvider>();\n\n constructor(primary: EmbeddingProvider, primaryName: string) {\n this.primary = primary;\n this.primaryName = primaryName;\n }\n\n /**\n * Register an additional embedding model.\n * Skips models that are disabled (enabled === false).\n * Skips registration if a model with the same name already exists.\n */\n register(config: EmbeddingModelConfig): void {\n if (config.enabled === false) return;\n // Prevent duplicate registrations\n if (config.name === this.primaryName) return;\n if (this.modelConfigs.some((m) => m.name === config.name)) return;\n this.modelConfigs.push(config);\n }\n\n /**\n * Get all registered model configs (additional models only; does not include primary).\n */\n getModels(): EmbeddingModelConfig[] {\n return [...this.modelConfigs];\n }\n\n /**\n * Get the primary model config-like object.\n */\n getPrimaryModel(): { name: string; provider: string; dimensions: number } {\n return {\n name: this.primaryName,\n provider: this.primary.constructor.name,\n dimensions: this.primary.dimensions,\n };\n }\n\n /** Whether any additional models are registered (multi-model mode). */\n isMultiModel(): boolean {\n return this.modelConfigs.length > 0;\n }\n\n /**\n * Embed text with a specific model by name.\n * Pass undefined or primaryName to use the primary model.\n */\n async embed(text: string, modelName?: string): Promise<Float32Array> {\n if (!modelName || modelName === this.primaryName) {\n const vec = await this.primary.embed(text);\n return toFloat32Array(vec);\n }\n const provider = this.getOrCreateProvider(modelName);\n const vec = await provider.embed(text);\n return toFloat32Array(vec);\n }\n\n /**\n * Embed text with ALL registered models (primary + additional).\n * Returns a Map from model name → Float32Array.\n * On partial failure, reports each error via capturePluginError and returns\n * whatever succeeded so callers get partial results instead of losing all data.\n */\n async embedAll(text: string): Promise<Map<string, Float32Array>> {\n const result = new Map<string, Float32Array>();\n\n // Primary model first\n const primaryVec = await this.primary.embed(text);\n result.set(this.primaryName, toFloat32Array(primaryVec));\n\n // Additional models in parallel\n if (this.modelConfigs.length > 0) {\n const tasks = this.modelConfigs.map(async (cfg) => {\n const provider = this.getOrCreateProvider(cfg.name);\n const vec = await provider.embed(text);\n return [cfg.name, toFloat32Array(vec)] as const;\n });\n const settled = await Promise.allSettled(tasks);\n for (const s of settled) {\n if (s.status === \"fulfilled\") {\n const [name, vec] = s.value;\n result.set(name, vec);\n } else {\n capturePluginError(s.reason instanceof Error ? s.reason : new Error(String(s.reason)), {\n subsystem: \"embedding-registry\",\n operation: \"embedAll\",\n });\n }\n }\n }\n\n return result;\n }\n\n /**\n * Return the names of all models (primary + additional enabled models).\n */\n allModelNames(): string[] {\n return [this.primaryName, ...this.modelConfigs.map((m) => m.name)];\n }\n\n // ---------------------------------------------------------------------------\n // Internal: lazy provider creation\n // ---------------------------------------------------------------------------\n\n private getOrCreateProvider(modelName: string): EmbeddingProvider {\n const existing = this.providers.get(modelName);\n if (existing) return existing;\n\n const cfg = this.modelConfigs.find((m) => m.name === modelName);\n if (!cfg) {\n throw new Error(`No embedding model registered with name '${modelName}'`);\n }\n\n const provider = createProviderForConfig(cfg);\n this.providers.set(modelName, provider);\n return provider;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Helper: create an EmbeddingProvider from an EmbeddingModelConfig\n// ---------------------------------------------------------------------------\n\nfunction createProviderForConfig(cfg: EmbeddingModelConfig): EmbeddingProvider {\n if (cfg.provider === \"ollama\") {\n return new OllamaEmbeddingProvider({\n model: cfg.name,\n dimensions: cfg.dimensions,\n endpoint: cfg.endpoint,\n });\n }\n\n if (cfg.provider === \"openai\") {\n if (!cfg.apiKey) {\n throw new Error(`EmbeddingModelConfig for '${cfg.name}': apiKey is required for openai provider`);\n }\n const client = new OpenAI({ apiKey: cfg.apiKey });\n return new Embeddings(client, cfg.name, cfg.dimensions);\n }\n\n if (cfg.provider === \"onnx\") {\n return new OnnxEmbeddingProvider({\n model: cfg.name,\n dimensions: cfg.dimensions,\n });\n }\n\n throw new Error(`EmbeddingModelConfig for '${cfg.name}': unknown provider '${cfg.provider as string}'`);\n}\n\n// ---------------------------------------------------------------------------\n// Utility\n// ---------------------------------------------------------------------------\n\n/** Convert number[] or Float32Array to Float32Array. */\nexport function toFloat32Array(vec: number[] | Float32Array): Float32Array {\n return vec instanceof Float32Array ? vec : new Float32Array(vec);\n}\n\n/**\n * Build an EmbeddingRegistry from plugin config.\n * If cfg.multiModels is empty/undefined, returns a single-model registry.\n */\nexport function buildEmbeddingRegistry(\n primary: EmbeddingProvider,\n primaryName: string,\n multiModels?: EmbeddingModelConfig[],\n): EmbeddingRegistry {\n const registry = new EmbeddingRegistry(primary, primaryName);\n if (multiModels && multiModels.length > 0) {\n for (const modelCfg of multiModels) {\n registry.register(modelCfg);\n }\n }\n return registry;\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chain-provider.js","names":[],"sources":["../../../services/embeddings/chain-provider.ts"],"sourcesContent":["/**\n * ChainEmbeddingProvider: tries a list of providers in order; first success wins.\n */\n\nimport { capturePluginError } from \"../error-reporter.js\";\nimport { isConfigError, shouldSuppressEmbeddingError } from \"./shared.js\";\nimport type { EmbeddingProvider } from \"./types.js\";\nimport { AllEmbeddingProvidersFailed } from \"./types.js\";\n\n/**\n * Tries a list of embedding providers in order; first success wins (no retry of earlier providers).\n * Aligns with LLM failover: same idea as getLLMModelPreference / tier; Ollama can be first tier.\n */\nexport class ChainEmbeddingProvider implements EmbeddingProvider {\n private readonly providers: EmbeddingProvider[];\n private readonly labels: string[];\n private activeIndex = 0;\n /** Per-provider cooldown: maps provider index → { timestamp until which it should be skipped, original error }.\n * Config errors (401/403/404) mark a provider as failed for CHAIN_PROVIDER_COOLDOWN_MS so we\n * don't waste a round-trip retrying a known-broken provider on every call (#385 Bug 4). */\n private readonly failedUntil = new Map<number, { expiry: number; error: Error }>();\n private static readonly COOLDOWN_MS = 60_000; // 60s, matches FallbackEmbeddingProvider.retryIntervalMs\n readonly dimensions: number;\n modelName: string;\n get activeProvider(): string {\n // When the active provider is itself a FallbackEmbeddingProvider that has switched internally,\n // prefer its own activeProvider over the chain's label for accurate reporting (#560).\n // The `?.activeProvider` can be undefined: EmbeddingProvider.activeProvider is optional\n // (`?: string`), so providers that don't implement it will hit the `??` fallback and return\n // the chain's own label. Both branches are intentionally reachable.\n return this.providers[this.activeIndex]?.activeProvider ?? this.labels[this.activeIndex];\n }\n\n constructor(providers: EmbeddingProvider[], labels: string[]) {\n if (providers.length === 0 || providers.length !== labels.length) {\n throw new Error(\"ChainEmbeddingProvider requires non-empty providers and same-length labels\");\n }\n const dim = providers[0].dimensions;\n if (providers.some((p) => p.dimensions !== dim)) {\n throw new Error(\"ChainEmbeddingProvider: all providers must have the same dimensions\");\n }\n this.providers = providers;\n this.labels = labels;\n this.dimensions = dim;\n this.modelName = providers[0].modelName;\n }\n\n private async tryProviders<T>(fn: (provider: EmbeddingProvider) => Promise<T>, phase: string): Promise<T> {\n let currentIndex = 0;\n this.modelName = this.providers[0].modelName;\n const collectedErrors: Error[] = [];\n while (currentIndex < this.providers.length) {\n // Skip providers in cooldown (config errors like 401/403/404 or transient errors). Expire stale entries.\n const cooldownEntry = this.failedUntil.get(currentIndex);\n if (cooldownEntry !== undefined) {\n if (Date.now() < cooldownEntry.expiry) {\n // Still in cooldown — add the original error to collectedErrors so safeEmbed can suppress correctly\n collectedErrors.push(cooldownEntry.error);\n currentIndex++;\n if (currentIndex < this.providers.length) {\n this.modelName = this.providers[currentIndex].modelName;\n }\n continue;\n }\n // Cooldown expired — let this provider retry\n this.failedUntil.delete(currentIndex);\n }\n try {\n const result = await fn(this.providers[currentIndex]);\n // Success — clear any lingering cooldown (belt-and-suspenders)\n this.failedUntil.delete(currentIndex);\n this.activeIndex = currentIndex;\n return result;\n } catch (err) {\n const asErr = err instanceof Error ? err : new Error(String(err));\n collectedErrors.push(asErr);\n // Mark provider as failed for cooldown period when it's a config error\n if (isConfigError(asErr)) {\n this.failedUntil.set(currentIndex, { expiry: Date.now() + ChainEmbeddingProvider.COOLDOWN_MS, error: asErr });\n }\n // Only capture individual provider failures when there are remaining fallbacks.\n // When this is the last provider, we'll degrade gracefully via AllEmbeddingProvidersFailed.\n const isLast = currentIndex + 1 >= this.providers.length;\n if (!isLast) {\n // Skip reporting config errors (404/403/401 — operator issues), 429 (rate limit), and circuit breaker open — not bugs (#329, #394, #385, #397, #458)\n if (!shouldSuppressEmbeddingError(asErr)) {\n capturePluginError(asErr, {\n subsystem: \"embeddings\",\n operation: \"chain-failover\",\n phase,\n });\n }\n }\n currentIndex++;\n if (currentIndex < this.providers.length) {\n this.modelName = this.providers[currentIndex].modelName;\n }\n }\n }\n // All providers exhausted — throw a typed error so callers can degrade gracefully\n // without reporting noise to error monitoring.\n throw new AllEmbeddingProvidersFailed(collectedErrors);\n }\n\n async embed(text: string): Promise<number[]> {\n return this.tryProviders((provider) => provider.embed(text), \"embed\");\n }\n\n async embedBatch(texts: string[]): Promise<number[][]> {\n return this.tryProviders((provider) => provider.embedBatch(texts), \"embedBatch\");\n }\n}\n"],"mappings":";;;;;;;;;;;AAaA,IAAa,yBAAb,MAAa,uBAAoD;CAC/D;CACA;CACA,cAAsB;;;;CAItB,8BAA+B,IAAI,
|
|
1
|
+
{"version":3,"file":"chain-provider.js","names":[],"sources":["../../../services/embeddings/chain-provider.ts"],"sourcesContent":["/**\n * ChainEmbeddingProvider: tries a list of providers in order; first success wins.\n */\n\nimport { capturePluginError } from \"../error-reporter.js\";\nimport { isConfigError, shouldSuppressEmbeddingError } from \"./shared.js\";\nimport type { EmbeddingProvider } from \"./types.js\";\nimport { AllEmbeddingProvidersFailed } from \"./types.js\";\n\n/**\n * Tries a list of embedding providers in order; first success wins (no retry of earlier providers).\n * Aligns with LLM failover: same idea as getLLMModelPreference / tier; Ollama can be first tier.\n */\nexport class ChainEmbeddingProvider implements EmbeddingProvider {\n private readonly providers: EmbeddingProvider[];\n private readonly labels: string[];\n private activeIndex = 0;\n /** Per-provider cooldown: maps provider index → { timestamp until which it should be skipped, original error }.\n * Config errors (401/403/404) mark a provider as failed for CHAIN_PROVIDER_COOLDOWN_MS so we\n * don't waste a round-trip retrying a known-broken provider on every call (#385 Bug 4). */\n private readonly failedUntil = new Map<number, { expiry: number; error: Error }>();\n private static readonly COOLDOWN_MS = 60_000; // 60s, matches FallbackEmbeddingProvider.retryIntervalMs\n readonly dimensions: number;\n modelName: string;\n get activeProvider(): string {\n // When the active provider is itself a FallbackEmbeddingProvider that has switched internally,\n // prefer its own activeProvider over the chain's label for accurate reporting (#560).\n // The `?.activeProvider` can be undefined: EmbeddingProvider.activeProvider is optional\n // (`?: string`), so providers that don't implement it will hit the `??` fallback and return\n // the chain's own label. Both branches are intentionally reachable.\n return this.providers[this.activeIndex]?.activeProvider ?? this.labels[this.activeIndex];\n }\n\n constructor(providers: EmbeddingProvider[], labels: string[]) {\n if (providers.length === 0 || providers.length !== labels.length) {\n throw new Error(\"ChainEmbeddingProvider requires non-empty providers and same-length labels\");\n }\n const dim = providers[0].dimensions;\n if (providers.some((p) => p.dimensions !== dim)) {\n throw new Error(\"ChainEmbeddingProvider: all providers must have the same dimensions\");\n }\n this.providers = providers;\n this.labels = labels;\n this.dimensions = dim;\n this.modelName = providers[0].modelName;\n }\n\n private async tryProviders<T>(fn: (provider: EmbeddingProvider) => Promise<T>, phase: string): Promise<T> {\n let currentIndex = 0;\n this.modelName = this.providers[0].modelName;\n const collectedErrors: Error[] = [];\n while (currentIndex < this.providers.length) {\n // Skip providers in cooldown (config errors like 401/403/404 or transient errors). Expire stale entries.\n const cooldownEntry = this.failedUntil.get(currentIndex);\n if (cooldownEntry !== undefined) {\n if (Date.now() < cooldownEntry.expiry) {\n // Still in cooldown — add the original error to collectedErrors so safeEmbed can suppress correctly\n collectedErrors.push(cooldownEntry.error);\n currentIndex++;\n if (currentIndex < this.providers.length) {\n this.modelName = this.providers[currentIndex].modelName;\n }\n continue;\n }\n // Cooldown expired — let this provider retry\n this.failedUntil.delete(currentIndex);\n }\n try {\n const result = await fn(this.providers[currentIndex]);\n // Success — clear any lingering cooldown (belt-and-suspenders)\n this.failedUntil.delete(currentIndex);\n this.activeIndex = currentIndex;\n return result;\n } catch (err) {\n const asErr = err instanceof Error ? err : new Error(String(err));\n collectedErrors.push(asErr);\n // Mark provider as failed for cooldown period when it's a config error\n if (isConfigError(asErr)) {\n this.failedUntil.set(currentIndex, { expiry: Date.now() + ChainEmbeddingProvider.COOLDOWN_MS, error: asErr });\n }\n // Only capture individual provider failures when there are remaining fallbacks.\n // When this is the last provider, we'll degrade gracefully via AllEmbeddingProvidersFailed.\n const isLast = currentIndex + 1 >= this.providers.length;\n if (!isLast) {\n // Skip reporting config errors (404/403/401 — operator issues), 429 (rate limit), and circuit breaker open — not bugs (#329, #394, #385, #397, #458)\n if (!shouldSuppressEmbeddingError(asErr)) {\n capturePluginError(asErr, {\n subsystem: \"embeddings\",\n operation: \"chain-failover\",\n phase,\n });\n }\n }\n currentIndex++;\n if (currentIndex < this.providers.length) {\n this.modelName = this.providers[currentIndex].modelName;\n }\n }\n }\n // All providers exhausted — throw a typed error so callers can degrade gracefully\n // without reporting noise to error monitoring.\n throw new AllEmbeddingProvidersFailed(collectedErrors);\n }\n\n async embed(text: string): Promise<number[]> {\n return this.tryProviders((provider) => provider.embed(text), \"embed\");\n }\n\n async embedBatch(texts: string[]): Promise<number[][]> {\n return this.tryProviders((provider) => provider.embedBatch(texts), \"embedBatch\");\n }\n}\n"],"mappings":";;;;;;;;;;;AAaA,IAAa,yBAAb,MAAa,uBAAoD;CAC/D;CACA;CACA,cAAsB;;;;CAItB,8BAA+B,IAAI,IAA8C;CACjF,OAAwB,cAAc;CACtC;CACA;CACA,IAAI,iBAAyB;EAM3B,OAAO,KAAK,UAAU,KAAK,cAAc,kBAAkB,KAAK,OAAO,KAAK;CAC9E;CAEA,YAAY,WAAgC,QAAkB;EAC5D,IAAI,UAAU,WAAW,KAAK,UAAU,WAAW,OAAO,QACxD,MAAM,IAAI,MAAM,4EAA4E;EAE9F,MAAM,MAAM,UAAU,GAAG;EACzB,IAAI,UAAU,MAAM,MAAM,EAAE,eAAe,GAAG,GAC5C,MAAM,IAAI,MAAM,qEAAqE;EAEvF,KAAK,YAAY;EACjB,KAAK,SAAS;EACd,KAAK,aAAa;EAClB,KAAK,YAAY,UAAU,GAAG;CAChC;CAEA,MAAc,aAAgB,IAAiD,OAA2B;EACxG,IAAI,eAAe;EACnB,KAAK,YAAY,KAAK,UAAU,GAAG;EACnC,MAAM,kBAA2B,CAAC;EAClC,OAAO,eAAe,KAAK,UAAU,QAAQ;GAE3C,MAAM,gBAAgB,KAAK,YAAY,IAAI,YAAY;GACvD,IAAI,kBAAkB,KAAA,GAAW;IAC/B,IAAI,KAAK,IAAI,IAAI,cAAc,QAAQ;KAErC,gBAAgB,KAAK,cAAc,KAAK;KACxC;KACA,IAAI,eAAe,KAAK,UAAU,QAChC,KAAK,YAAY,KAAK,UAAU,cAAc;KAEhD;IACF;IAEA,KAAK,YAAY,OAAO,YAAY;GACtC;GACA,IAAI;IACF,MAAM,SAAS,MAAM,GAAG,KAAK,UAAU,aAAa;IAEpD,KAAK,YAAY,OAAO,YAAY;IACpC,KAAK,cAAc;IACnB,OAAO;GACT,SAAS,KAAK;IACZ,MAAM,QAAQ,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,GAAG,CAAC;IAChE,gBAAgB,KAAK,KAAK;IAE1B,IAAI,cAAc,KAAK,GACrB,KAAK,YAAY,IAAI,cAAc;KAAE,QAAQ,KAAK,IAAI,IAAI,uBAAuB;KAAa,OAAO;IAAM,CAAC;IAK9G,IAAI,EADW,eAAe,KAAK,KAAK,UAAU;SAG5C,CAAC,6BAA6B,KAAK,GACrC,mBAAmB,OAAO;MACxB,WAAW;MACX,WAAW;MACX;KACF,CAAC;IAAA;IAGL;IACA,IAAI,eAAe,KAAK,UAAU,QAChC,KAAK,YAAY,KAAK,UAAU,cAAc;GAElD;EACF;EAGA,MAAM,IAAI,4BAA4B,eAAe;CACvD;CAEA,MAAM,MAAM,MAAiC;EAC3C,OAAO,KAAK,cAAc,aAAa,SAAS,MAAM,IAAI,GAAG,OAAO;CACtE;CAEA,MAAM,WAAW,OAAsC;EACrD,OAAO,KAAK,cAAc,aAAa,SAAS,WAAW,KAAK,GAAG,YAAY;CACjF;AACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"factory.js","names":[],"sources":["../../../services/embeddings/factory.ts"],"sourcesContent":["/**\n * Factory function: creates the right EmbeddingProvider from plugin config.\n */\n\nimport OpenAI from \"openai\";\nimport { createApimGatewayFetch, isAzureApiManagementGatewayUrl } from \"../../utils/apim-gateway-fetch.js\";\nimport { pluginLogger } from \"../../utils/logger.js\";\nimport { capturePluginError } from \"../error-reporter.js\";\nimport { ChainEmbeddingProvider } from \"./chain-provider.js\";\nimport { FallbackEmbeddingProvider } from \"./fallback-provider.js\";\nimport { OllamaEmbeddingProvider } from \"./ollama-provider.js\";\nimport { OnnxEmbeddingProvider, isOnnxRuntimeMissingError } from \"./onnx-provider.js\";\nimport { Embeddings } from \"./openai-provider.js\";\nimport {\n GOOGLE_EMBEDDING_BASE_URL,\n GOOGLE_EMBED_DEFAULT_DIMENSIONS,\n GOOGLE_EMBED_DEFAULT_MODEL,\n KNOWN_GOOGLE_EMBED_MODELS,\n OPENAI_ONLY_EMBED_MODELS,\n isAzureOpenAiCompatibleEndpoint,\n isAzureOpenAiResourceEndpoint,\n} from \"./shared.js\";\nimport type { EmbeddingConfig, EmbeddingProvider } from \"./types.js\";\n\n/** Classic Azure OpenAI REST (e.g. `/openai/deployments/...`) uses this query param. `/openai/v1` compat endpoints reject it (400). */\nexport const AZURE_OPENAI_API_VERSION = \"2024-10-21\";\n\n/**\n * Build OpenAI client options for the openai embedding provider.\n * When endpoint is set, uses it as baseURL; when it's Azure, adds the api-key header and (for non-v1 REST paths) api-version.\n * Azure resource roots must use /openai/v1 (not bare /v1). Deployment-style URLs (/openai/deployments/...) are left as-is.\n */\nfunction openaiEmbeddingClientOpts(\n apiKey: string,\n endpoint?: string,\n): {\n apiKey: string;\n baseURL?: string;\n defaultHeaders?: Record<string, string>;\n defaultQuery?: Record<string, string>;\n fetch?: typeof globalThis.fetch;\n} {\n const opts: {\n apiKey: string;\n baseURL?: string;\n defaultHeaders?: Record<string, string>;\n defaultQuery?: Record<string, string>;\n fetch?: typeof globalThis.fetch;\n } = { apiKey };\n if (typeof endpoint === \"string\" && endpoint.trim().length > 0) {\n const baseURL = endpoint.trim().replace(/\\/+$/, \"\");\n const isAzureDeploymentPath = /\\/openai\\/deployments\\//i.test(baseURL);\n const hasOpenAiV1Path = /\\/openai\\/v1(?:\\/|$)/i.test(baseURL);\n if (hasOpenAiV1Path || (isAzureOpenAiResourceEndpoint(baseURL) && isAzureDeploymentPath)) {\n opts.baseURL = baseURL;\n } else if (isAzureOpenAiResourceEndpoint(baseURL) && !isAzureDeploymentPath) {\n const endsWithOpenAi = /\\/openai$/i.test(baseURL);\n opts.baseURL = endsWithOpenAi ? `${baseURL}/v1` : `${baseURL}/openai/v1`;\n } else if (isAzureApiManagementGatewayUrl(baseURL) && isAzureDeploymentPath) {\n opts.baseURL = baseURL;\n } else if (isAzureApiManagementGatewayUrl(baseURL) && !isAzureDeploymentPath) {\n const endsWithOpenAi = /\\/openai$/i.test(baseURL);\n opts.baseURL = endsWithOpenAi ? `${baseURL}/v1` : `${baseURL}/openai/v1`;\n } else {\n opts.baseURL = baseURL.includes(\"/v1\") ? baseURL : `${baseURL}/v1`;\n }\n if (isAzureOpenAiResourceEndpoint(opts.baseURL)) {\n opts.defaultHeaders = { \"api-key\": apiKey };\n const openAiV1Compat = /\\/openai\\/v1(?:\\/|$)/i.test(opts.baseURL);\n // Foundry / Azure AI: `/openai/v1/*` returns 400 \"API version not supported\" when `api-version` is present.\n if (!openAiV1Compat) {\n opts.defaultQuery = { \"api-version\": AZURE_OPENAI_API_VERSION };\n }\n }\n // API Management gateway: same api-key auth as Azure resource, but SDK Bearer breaks auth — strip it in fetch.\n if (opts.baseURL && isAzureApiManagementGatewayUrl(opts.baseURL)) {\n opts.defaultHeaders = { ...(opts.defaultHeaders ?? {}), \"api-key\": apiKey };\n opts.fetch = createApimGatewayFetch(apiKey);\n const openAiV1Compat = /\\/openai\\/v1(?:\\/|$)/i.test(opts.baseURL);\n if (!openAiV1Compat) {\n opts.defaultQuery = { \"api-version\": AZURE_OPENAI_API_VERSION };\n }\n }\n }\n return opts;\n}\n\n/** Local/Ollama/ONNX embedding ids — never use as OpenAI/Azure `model` when falling back from Ollama/ONNX (#932). */\nfunction isLocalOnlyEmbeddingModelId(model: string | undefined): boolean {\n if (!model) return false;\n switch (model) {\n case \"nomic-embed-text\":\n case \"mxbai-embed-large\":\n case \"bge-m3\":\n case \"bge-large\":\n case \"bge-small-en-v1.5\":\n case \"snowflake-arctic-embed\":\n case \"all-minilm\":\n case \"all-MiniLM-L6-v2\":\n return true;\n default:\n return false;\n }\n}\n\n/** API model id(s) for OpenAI-compatible embeddings: optional Azure deployment name overrides logical `model`. */\nfunction openAiEmbeddingApiModels(cfg: EmbeddingConfig, forFallback = false): string[] {\n const { model, models, deployment } = cfg;\n if (deployment && deployment.trim().length > 0) {\n return [deployment.trim()];\n }\n if (forFallback && !models?.length) {\n if (model && OPENAI_ONLY_EMBED_MODELS.has(model)) {\n return [model];\n }\n // Fallback path used for chain OpenAI arm and Ollama/ONNX→OpenAI fallback. Azure deployment names\n // are often not in OPENAI_ONLY_EMBED_MODELS; do not substitute text-embedding-3-small (#932).\n const m = typeof model === \"string\" ? model.trim() : \"\";\n if (m && isAzureOpenAiCompatibleEndpoint(cfg.endpoint) && !isLocalOnlyEmbeddingModelId(m)) {\n return [m];\n }\n return [\"text-embedding-3-small\"];\n }\n return models?.length ? models : [model];\n}\n\n/** When using Azure `deployment` as the API model id, pass `cfg.model` for dimension limits and `dimensions` param. */\nfunction azureEmbeddingLogicalModelHint(cfg: EmbeddingConfig): string | undefined {\n return cfg.deployment && cfg.deployment.trim().length > 0 ? cfg.model : undefined;\n}\n\n/** Some Azure/APIM hosts reject the optional `dimensions` field on embeddings.create (#948). */\nfunction omitEmbeddingDimensionsForAzure(cfg: EmbeddingConfig): boolean {\n return isAzureOpenAiCompatibleEndpoint(cfg.endpoint);\n}\n\n/**\n * Factory: creates the right EmbeddingProvider from plugin config.\n * - When embedding.preferredProviders has length > 1: chain (try in order; aligns with LLM failover, Ollama-as-tier).\n * - provider='ollama' → OllamaEmbeddingProvider (with optional OpenAI fallback if apiKey set)\n * - provider='openai' → Embeddings (OpenAI)\n * - provider='onnx' → OnnxEmbeddingProvider (with optional OpenAI fallback if apiKey set)\n */\nexport function createEmbeddingProvider(cfg: EmbeddingConfig, onFallback?: (err: unknown) => void): EmbeddingProvider {\n const { provider, model, apiKey, models, dimensions, endpoint, batchSize, preferredProviders, retryIntervalMs } = cfg;\n const openaiApiModels = openAiEmbeddingApiModels(cfg);\n\n if (preferredProviders && preferredProviders.length > 1) {\n const chain: EmbeddingProvider[] = [];\n const labels: string[] = [];\n // When Google is in the chain with an OpenAI-only model name, we use 768 for Google; chain requires same dimensions for all.\n const googleInChainWithOpenAiModel =\n preferredProviders.includes(\"google\") && model && OPENAI_ONLY_EMBED_MODELS.has(model);\n const chainDimensions = googleInChainWithOpenAiModel ? GOOGLE_EMBED_DEFAULT_DIMENSIONS : dimensions;\n if (googleInChainWithOpenAiModel && dimensions && dimensions !== GOOGLE_EMBED_DEFAULT_DIMENSIONS) {\n pluginLogger.warn(\n `memory-hybrid: embedding chain includes Google with OpenAI model '${model}' — ` +\n `forcing dimensions to ${GOOGLE_EMBED_DEFAULT_DIMENSIONS} (Google default) instead of ${dimensions}. ` +\n `Set embedding.preferredProviders: [\"openai\"] to use ${dimensions}-dim OpenAI-only embeddings, ` +\n `or set embedding.dimensions: ${GOOGLE_EMBED_DEFAULT_DIMENSIONS} to acknowledge the Google chain dimensions.`,\n );\n }\n const ollamaModel =\n model && ![\"text-embedding-3-small\", \"text-embedding-3-large\", \"text-embedding-ada-002\"].includes(model)\n ? model\n : \"nomic-embed-text\";\n const googleModel = model && KNOWN_GOOGLE_EMBED_MODELS.has(model) ? model : GOOGLE_EMBED_DEFAULT_MODEL;\n const openaiChainModels = openAiEmbeddingApiModels(cfg, true);\n for (const name of preferredProviders) {\n if (name === \"ollama\") {\n try {\n chain.push(\n new OllamaEmbeddingProvider({ model: ollamaModel, dimensions: chainDimensions, endpoint, batchSize }),\n );\n labels.push(\"ollama\");\n } catch (err) {\n capturePluginError(err instanceof Error ? err : new Error(String(err)), {\n subsystem: \"embeddings\",\n operation: \"chain-build-ollama\",\n });\n }\n } else if (name === \"openai\" && apiKey) {\n try {\n const client = new OpenAI(openaiEmbeddingClientOpts(apiKey, endpoint));\n chain.push(\n new Embeddings(\n client,\n openaiChainModels,\n chainDimensions,\n batchSize,\n azureEmbeddingLogicalModelHint(cfg),\n omitEmbeddingDimensionsForAzure(cfg),\n ),\n );\n labels.push(\"openai\");\n } catch (err) {\n capturePluginError(err instanceof Error ? err : new Error(String(err)), {\n subsystem: \"embeddings\",\n operation: \"chain-build-openai\",\n });\n }\n } else if (name === \"google\" && cfg.googleApiKey && cfg.googleApiKey.length >= 10) {\n try {\n const client = new OpenAI({ apiKey: cfg.googleApiKey, baseURL: GOOGLE_EMBEDDING_BASE_URL });\n chain.push(new Embeddings(client, googleModel, chainDimensions, batchSize));\n labels.push(\"google\");\n } catch (err) {\n capturePluginError(err instanceof Error ? err : new Error(String(err)), {\n subsystem: \"embeddings\",\n operation: \"chain-build-google\",\n });\n }\n }\n }\n if (chain.length === 0) {\n throw new Error(\n \"embedding.preferredProviders: no provider could be built (check apiKey for openai/google, Ollama for ollama, distill.apiKey or llm.providers.google for Google).\",\n );\n }\n if (chain.length === 1) {\n return chain[0];\n }\n return new ChainEmbeddingProvider(chain, labels);\n }\n\n if (provider === \"ollama\") {\n const primary = new OllamaEmbeddingProvider({ model, dimensions, endpoint, batchSize });\n // Optional fallback to OpenAI when a key is provided\n if (apiKey) {\n const openaiClient = new OpenAI(openaiEmbeddingClientOpts(apiKey, endpoint));\n try {\n const fallback = new Embeddings(\n openaiClient,\n openAiEmbeddingApiModels(cfg, true),\n dimensions,\n batchSize,\n azureEmbeddingLogicalModelHint(cfg),\n omitEmbeddingDimensionsForAzure(cfg),\n );\n return new FallbackEmbeddingProvider(primary, fallback, onFallback, \"ollama\", \"openai\", retryIntervalMs);\n } catch (err) {\n // Fallback creation failed (e.g. Ollama dimensions exceed all OpenAI model limits).\n // Warn the user so they know their fallback isn't working.\n pluginLogger.warn(\n `memory-hybrid: Failed to create OpenAI fallback for Ollama provider: ${err instanceof Error ? err.message : String(err)}. Continuing with Ollama-only (no fallback).`,\n );\n return primary;\n }\n }\n return primary;\n }\n\n if (provider === \"openai\") {\n if (!apiKey) throw new Error(\"OpenAI embedding provider requires embedding.apiKey\");\n const openaiClient = new OpenAI(openaiEmbeddingClientOpts(apiKey, endpoint));\n return new Embeddings(\n openaiClient,\n openaiApiModels,\n dimensions,\n batchSize,\n azureEmbeddingLogicalModelHint(cfg),\n omitEmbeddingDimensionsForAzure(cfg),\n );\n }\n\n if (provider === \"google\") {\n if (!cfg.googleApiKey || cfg.googleApiKey.length < 10) {\n throw new Error(\"Google embedding provider requires distill.apiKey or llm.providers.google.apiKey.\");\n }\n const client = new OpenAI({ apiKey: cfg.googleApiKey, baseURL: GOOGLE_EMBEDDING_BASE_URL });\n // Use configured model only when it is a known Google embedding model; otherwise default.\n // When config has an OpenAI-only model name (e.g. text-embedding-3-large), use 768 dims so the API and vectors match.\n const googleEmbedModel = model && KNOWN_GOOGLE_EMBED_MODELS.has(model) ? model : GOOGLE_EMBED_DEFAULT_MODEL;\n const googleDimensions =\n model && OPENAI_ONLY_EMBED_MODELS.has(model) ? GOOGLE_EMBED_DEFAULT_DIMENSIONS : dimensions;\n return new Embeddings(client, googleEmbedModel, googleDimensions, batchSize);\n }\n\n if (provider === \"onnx\") {\n const primary = new OnnxEmbeddingProvider({ model, dimensions, batchSize });\n if (apiKey) {\n const openaiClient = new OpenAI(openaiEmbeddingClientOpts(apiKey, endpoint));\n try {\n const fallback = new Embeddings(\n openaiClient,\n openAiEmbeddingApiModels(cfg, true),\n dimensions,\n batchSize,\n azureEmbeddingLogicalModelHint(cfg),\n omitEmbeddingDimensionsForAzure(cfg),\n );\n const onSwitch = (err: unknown) => {\n if (isOnnxRuntimeMissingError(err)) {\n pluginLogger.warn(\"memory-hybrid: onnxruntime-node not installed; falling back to OpenAI embeddings.\");\n } else {\n pluginLogger.warn(\n `memory-hybrid: ONNX embeddings failed; falling back to OpenAI. ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n onFallback?.(err);\n };\n return new FallbackEmbeddingProvider(primary, fallback, onSwitch, \"onnx\", \"openai\", retryIntervalMs);\n } catch (err) {\n pluginLogger.warn(\n `memory-hybrid: Failed to create OpenAI fallback for ONNX provider: ${err instanceof Error ? err.message : String(err)}. Continuing with ONNX-only (no fallback).`,\n );\n return primary;\n }\n }\n return primary;\n }\n\n throw new Error(`Unknown embedding provider: '${provider as string}'. Valid options: openai, ollama, onnx, google.`);\n}\n"],"mappings":";;;;;;;;;;;;;;;AAyBA,MAAa,2BAA2B;;;;;;AAOxC,SAAS,0BACP,QACA,UAOA;CACA,MAAM,OAMF,EAAE,QAAQ;CACd,IAAI,OAAO,aAAa,YAAY,SAAS,MAAM,CAAC,SAAS,GAAG;EAC9D,MAAM,UAAU,SAAS,MAAM,CAAC,QAAQ,QAAQ,GAAG;EACnD,MAAM,wBAAwB,2BAA2B,KAAK,QAAQ;EAEtE,IADwB,wBAAwB,KAAK,QAClC,IAAK,8BAA8B,QAAQ,IAAI,uBAChE,KAAK,UAAU;OACV,IAAI,8BAA8B,QAAQ,IAAI,CAAC,uBAEpD,KAAK,UADkB,aAAa,KAAK,QACZ,GAAG,GAAG,QAAQ,OAAO,GAAG,QAAQ;OACxD,IAAI,+BAA+B,QAAQ,IAAI,uBACpD,KAAK,UAAU;OACV,IAAI,+BAA+B,QAAQ,IAAI,CAAC,uBAErD,KAAK,UADkB,aAAa,KAAK,QACZ,GAAG,GAAG,QAAQ,OAAO,GAAG,QAAQ;OAE7D,KAAK,UAAU,QAAQ,SAAS,MAAM,GAAG,UAAU,GAAG,QAAQ;EAEhE,IAAI,8BAA8B,KAAK,QAAQ,EAAE;GAC/C,KAAK,iBAAiB,EAAE,WAAW,QAAQ;GAG3C,IAAI,CAFmB,wBAAwB,KAAK,KAAK,QAEtC,EACjB,KAAK,eAAe,EAAE,eAAe,0BAA0B;;EAInE,IAAI,KAAK,WAAW,+BAA+B,KAAK,QAAQ,EAAE;GAChE,KAAK,iBAAiB;IAAE,GAAI,KAAK,kBAAkB,EAAE;IAAG,WAAW;IAAQ;GAC3E,KAAK,QAAQ,uBAAuB,OAAO;GAE3C,IAAI,CADmB,wBAAwB,KAAK,KAAK,QACtC,EACjB,KAAK,eAAe,EAAE,eAAe,0BAA0B;;;CAIrE,OAAO;;;AAIT,SAAS,4BAA4B,OAAoC;CACvE,IAAI,CAAC,OAAO,OAAO;CACnB,QAAQ,OAAR;EACE,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK,oBACH,OAAO;EACT,SACE,OAAO;;;;AAKb,SAAS,yBAAyB,KAAsB,cAAc,OAAiB;CACrF,MAAM,EAAE,OAAO,QAAQ,eAAe;CACtC,IAAI,cAAc,WAAW,MAAM,CAAC,SAAS,GAC3C,OAAO,CAAC,WAAW,MAAM,CAAC;CAE5B,IAAI,eAAe,CAAC,QAAQ,QAAQ;EAClC,IAAI,SAAS,yBAAyB,IAAI,MAAM,EAC9C,OAAO,CAAC,MAAM;EAIhB,MAAM,IAAI,OAAO,UAAU,WAAW,MAAM,MAAM,GAAG;EACrD,IAAI,KAAK,gCAAgC,IAAI,SAAS,IAAI,CAAC,4BAA4B,EAAE,EACvF,OAAO,CAAC,EAAE;EAEZ,OAAO,CAAC,yBAAyB;;CAEnC,OAAO,QAAQ,SAAS,SAAS,CAAC,MAAM;;;AAI1C,SAAS,+BAA+B,KAA0C;CAChF,OAAO,IAAI,cAAc,IAAI,WAAW,MAAM,CAAC,SAAS,IAAI,IAAI,QAAQ,KAAA;;;AAI1E,SAAS,gCAAgC,KAA+B;CACtE,OAAO,gCAAgC,IAAI,SAAS;;;;;;;;;AAUtD,SAAgB,wBAAwB,KAAsB,YAAwD;CACpH,MAAM,EAAE,UAAU,OAAO,QAAQ,QAAQ,YAAY,UAAU,WAAW,oBAAoB,oBAAoB;CAClH,MAAM,kBAAkB,yBAAyB,IAAI;CAErD,IAAI,sBAAsB,mBAAmB,SAAS,GAAG;EACvD,MAAM,QAA6B,EAAE;EACrC,MAAM,SAAmB,EAAE;EAE3B,MAAM,+BACJ,mBAAmB,SAAS,SAAS,IAAI,SAAS,yBAAyB,IAAI,MAAM;EACvF,MAAM,kBAAkB,+BAAA,MAAiE;EACzF,IAAI,gCAAgC,cAAc,eAAA,KAChD,aAAa,KACX,qEAAqE,MAAM,4DACe,WAAW,wDAC5C,WAAW,2GAErE;EAEH,MAAM,cACJ,SAAS,CAAC;GAAC;GAA0B;GAA0B;GAAyB,CAAC,SAAS,MAAM,GACpG,QACA;EACN,MAAM,cAAc,SAAS,0BAA0B,IAAI,MAAM,GAAG,QAAQ;EAC5E,MAAM,oBAAoB,yBAAyB,KAAK,KAAK;EAC7D,KAAK,MAAM,QAAQ,oBACjB,IAAI,SAAS,UACX,IAAI;GACF,MAAM,KACJ,IAAI,wBAAwB;IAAE,OAAO;IAAa,YAAY;IAAiB;IAAU;IAAW,CAAC,CACtG;GACD,OAAO,KAAK,SAAS;WACd,KAAK;GACZ,mBAAmB,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,IAAI,CAAC,EAAE;IACtE,WAAW;IACX,WAAW;IACZ,CAAC;;OAEC,IAAI,SAAS,YAAY,QAC9B,IAAI;GACF,MAAM,SAAS,IAAI,OAAO,0BAA0B,QAAQ,SAAS,CAAC;GACtE,MAAM,KACJ,IAAI,WACF,QACA,mBACA,iBACA,WACA,+BAA+B,IAAI,EACnC,gCAAgC,IAAI,CACrC,CACF;GACD,OAAO,KAAK,SAAS;WACd,KAAK;GACZ,mBAAmB,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,IAAI,CAAC,EAAE;IACtE,WAAW;IACX,WAAW;IACZ,CAAC;;OAEC,IAAI,SAAS,YAAY,IAAI,gBAAgB,IAAI,aAAa,UAAU,IAC7E,IAAI;GACF,MAAM,SAAS,IAAI,OAAO;IAAE,QAAQ,IAAI;IAAc,SAAS;IAA2B,CAAC;GAC3F,MAAM,KAAK,IAAI,WAAW,QAAQ,aAAa,iBAAiB,UAAU,CAAC;GAC3E,OAAO,KAAK,SAAS;WACd,KAAK;GACZ,mBAAmB,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,IAAI,CAAC,EAAE;IACtE,WAAW;IACX,WAAW;IACZ,CAAC;;EAIR,IAAI,MAAM,WAAW,GACnB,MAAM,IAAI,MACR,mKACD;EAEH,IAAI,MAAM,WAAW,GACnB,OAAO,MAAM;EAEf,OAAO,IAAI,uBAAuB,OAAO,OAAO;;CAGlD,IAAI,aAAa,UAAU;EACzB,MAAM,UAAU,IAAI,wBAAwB;GAAE;GAAO;GAAY;GAAU;GAAW,CAAC;EAEvF,IAAI,QAAQ;GACV,MAAM,eAAe,IAAI,OAAO,0BAA0B,QAAQ,SAAS,CAAC;GAC5E,IAAI;IASF,OAAO,IAAI,0BAA0B,SAAS,IARzB,WACnB,cACA,yBAAyB,KAAK,KAAK,EACnC,YACA,WACA,+BAA+B,IAAI,EACnC,gCAAgC,IAAI,CAEgB,EAAE,YAAY,UAAU,UAAU,gBAAgB;YACjG,KAAK;IAGZ,aAAa,KACX,wEAAwE,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,CAAC,8CAC1H;IACD,OAAO;;;EAGX,OAAO;;CAGT,IAAI,aAAa,UAAU;EACzB,IAAI,CAAC,QAAQ,MAAM,IAAI,MAAM,sDAAsD;EAEnF,OAAO,IAAI,WACT,IAFuB,OAAO,0BAA0B,QAAQ,SAAS,CAE7D,EACZ,iBACA,YACA,WACA,+BAA+B,IAAI,EACnC,gCAAgC,IAAI,CACrC;;CAGH,IAAI,aAAa,UAAU;EACzB,IAAI,CAAC,IAAI,gBAAgB,IAAI,aAAa,SAAS,IACjD,MAAM,IAAI,MAAM,oFAAoF;EAQtG,OAAO,IAAI,WAAW,IANH,OAAO;GAAE,QAAQ,IAAI;GAAc,SAAS;GAA2B,CAM9D,EAHH,SAAS,0BAA0B,IAAI,MAAM,GAAG,QAAQ,4BAE/E,SAAS,yBAAyB,IAAI,MAAM,GAAA,MAAqC,YACjB,UAAU;;CAG9E,IAAI,aAAa,QAAQ;EACvB,MAAM,UAAU,IAAI,sBAAsB;GAAE;GAAO;GAAY;GAAW,CAAC;EAC3E,IAAI,QAAQ;GACV,MAAM,eAAe,IAAI,OAAO,0BAA0B,QAAQ,SAAS,CAAC;GAC5E,IAAI;IACF,MAAM,WAAW,IAAI,WACnB,cACA,yBAAyB,KAAK,KAAK,EACnC,YACA,WACA,+BAA+B,IAAI,EACnC,gCAAgC,IAAI,CACrC;IACD,MAAM,YAAY,QAAiB;KACjC,IAAI,0BAA0B,IAAI,EAChC,aAAa,KAAK,oFAAoF;UAEtG,aAAa,KACX,kEAAkE,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,GACnH;KAEH,aAAa,IAAI;;IAEnB,OAAO,IAAI,0BAA0B,SAAS,UAAU,UAAU,QAAQ,UAAU,gBAAgB;YAC7F,KAAK;IACZ,aAAa,KACX,sEAAsE,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,CAAC,4CACxH;IACD,OAAO;;;EAGX,OAAO;;CAGT,MAAM,IAAI,MAAM,gCAAgC,SAAmB,iDAAiD"}
|
|
1
|
+
{"version":3,"file":"factory.js","names":[],"sources":["../../../services/embeddings/factory.ts"],"sourcesContent":["/**\n * Factory function: creates the right EmbeddingProvider from plugin config.\n */\n\nimport OpenAI from \"openai\";\nimport { createApimGatewayFetch, isAzureApiManagementGatewayUrl } from \"../../utils/apim-gateway-fetch.js\";\nimport { pluginLogger } from \"../../utils/logger.js\";\nimport { capturePluginError } from \"../error-reporter.js\";\nimport { ChainEmbeddingProvider } from \"./chain-provider.js\";\nimport { FallbackEmbeddingProvider } from \"./fallback-provider.js\";\nimport { OllamaEmbeddingProvider } from \"./ollama-provider.js\";\nimport { OnnxEmbeddingProvider, isOnnxRuntimeMissingError } from \"./onnx-provider.js\";\nimport { Embeddings } from \"./openai-provider.js\";\nimport {\n GOOGLE_EMBEDDING_BASE_URL,\n GOOGLE_EMBED_DEFAULT_DIMENSIONS,\n GOOGLE_EMBED_DEFAULT_MODEL,\n KNOWN_GOOGLE_EMBED_MODELS,\n OPENAI_ONLY_EMBED_MODELS,\n isAzureOpenAiCompatibleEndpoint,\n isAzureOpenAiResourceEndpoint,\n} from \"./shared.js\";\nimport type { EmbeddingConfig, EmbeddingProvider } from \"./types.js\";\n\n/** Classic Azure OpenAI REST (e.g. `/openai/deployments/...`) uses this query param. `/openai/v1` compat endpoints reject it (400). */\nexport const AZURE_OPENAI_API_VERSION = \"2024-10-21\";\n\n/**\n * Build OpenAI client options for the openai embedding provider.\n * When endpoint is set, uses it as baseURL; when it's Azure, adds the api-key header and (for non-v1 REST paths) api-version.\n * Azure resource roots must use /openai/v1 (not bare /v1). Deployment-style URLs (/openai/deployments/...) are left as-is.\n */\nfunction openaiEmbeddingClientOpts(\n apiKey: string,\n endpoint?: string,\n): {\n apiKey: string;\n baseURL?: string;\n defaultHeaders?: Record<string, string>;\n defaultQuery?: Record<string, string>;\n fetch?: typeof globalThis.fetch;\n} {\n const opts: {\n apiKey: string;\n baseURL?: string;\n defaultHeaders?: Record<string, string>;\n defaultQuery?: Record<string, string>;\n fetch?: typeof globalThis.fetch;\n } = { apiKey };\n if (typeof endpoint === \"string\" && endpoint.trim().length > 0) {\n const baseURL = endpoint.trim().replace(/\\/+$/, \"\");\n const isAzureDeploymentPath = /\\/openai\\/deployments\\//i.test(baseURL);\n const hasOpenAiV1Path = /\\/openai\\/v1(?:\\/|$)/i.test(baseURL);\n if (hasOpenAiV1Path || (isAzureOpenAiResourceEndpoint(baseURL) && isAzureDeploymentPath)) {\n opts.baseURL = baseURL;\n } else if (isAzureOpenAiResourceEndpoint(baseURL) && !isAzureDeploymentPath) {\n const endsWithOpenAi = /\\/openai$/i.test(baseURL);\n opts.baseURL = endsWithOpenAi ? `${baseURL}/v1` : `${baseURL}/openai/v1`;\n } else if (isAzureApiManagementGatewayUrl(baseURL) && isAzureDeploymentPath) {\n opts.baseURL = baseURL;\n } else if (isAzureApiManagementGatewayUrl(baseURL) && !isAzureDeploymentPath) {\n const endsWithOpenAi = /\\/openai$/i.test(baseURL);\n opts.baseURL = endsWithOpenAi ? `${baseURL}/v1` : `${baseURL}/openai/v1`;\n } else {\n opts.baseURL = baseURL.includes(\"/v1\") ? baseURL : `${baseURL}/v1`;\n }\n if (isAzureOpenAiResourceEndpoint(opts.baseURL)) {\n opts.defaultHeaders = { \"api-key\": apiKey };\n const openAiV1Compat = /\\/openai\\/v1(?:\\/|$)/i.test(opts.baseURL);\n // Foundry / Azure AI: `/openai/v1/*` returns 400 \"API version not supported\" when `api-version` is present.\n if (!openAiV1Compat) {\n opts.defaultQuery = { \"api-version\": AZURE_OPENAI_API_VERSION };\n }\n }\n // API Management gateway: same api-key auth as Azure resource, but SDK Bearer breaks auth — strip it in fetch.\n if (opts.baseURL && isAzureApiManagementGatewayUrl(opts.baseURL)) {\n opts.defaultHeaders = { ...(opts.defaultHeaders ?? {}), \"api-key\": apiKey };\n opts.fetch = createApimGatewayFetch(apiKey);\n const openAiV1Compat = /\\/openai\\/v1(?:\\/|$)/i.test(opts.baseURL);\n if (!openAiV1Compat) {\n opts.defaultQuery = { \"api-version\": AZURE_OPENAI_API_VERSION };\n }\n }\n }\n return opts;\n}\n\n/** Local/Ollama/ONNX embedding ids — never use as OpenAI/Azure `model` when falling back from Ollama/ONNX (#932). */\nfunction isLocalOnlyEmbeddingModelId(model: string | undefined): boolean {\n if (!model) return false;\n switch (model) {\n case \"nomic-embed-text\":\n case \"mxbai-embed-large\":\n case \"bge-m3\":\n case \"bge-large\":\n case \"bge-small-en-v1.5\":\n case \"snowflake-arctic-embed\":\n case \"all-minilm\":\n case \"all-MiniLM-L6-v2\":\n return true;\n default:\n return false;\n }\n}\n\n/** API model id(s) for OpenAI-compatible embeddings: optional Azure deployment name overrides logical `model`. */\nfunction openAiEmbeddingApiModels(cfg: EmbeddingConfig, forFallback = false): string[] {\n const { model, models, deployment } = cfg;\n if (deployment && deployment.trim().length > 0) {\n return [deployment.trim()];\n }\n if (forFallback && !models?.length) {\n if (model && OPENAI_ONLY_EMBED_MODELS.has(model)) {\n return [model];\n }\n // Fallback path used for chain OpenAI arm and Ollama/ONNX→OpenAI fallback. Azure deployment names\n // are often not in OPENAI_ONLY_EMBED_MODELS; do not substitute text-embedding-3-small (#932).\n const m = typeof model === \"string\" ? model.trim() : \"\";\n if (m && isAzureOpenAiCompatibleEndpoint(cfg.endpoint) && !isLocalOnlyEmbeddingModelId(m)) {\n return [m];\n }\n return [\"text-embedding-3-small\"];\n }\n return models?.length ? models : [model];\n}\n\n/** When using Azure `deployment` as the API model id, pass `cfg.model` for dimension limits and `dimensions` param. */\nfunction azureEmbeddingLogicalModelHint(cfg: EmbeddingConfig): string | undefined {\n return cfg.deployment && cfg.deployment.trim().length > 0 ? cfg.model : undefined;\n}\n\n/** Some Azure/APIM hosts reject the optional `dimensions` field on embeddings.create (#948). */\nfunction omitEmbeddingDimensionsForAzure(cfg: EmbeddingConfig): boolean {\n return isAzureOpenAiCompatibleEndpoint(cfg.endpoint);\n}\n\n/**\n * Factory: creates the right EmbeddingProvider from plugin config.\n * - When embedding.preferredProviders has length > 1: chain (try in order; aligns with LLM failover, Ollama-as-tier).\n * - provider='ollama' → OllamaEmbeddingProvider (with optional OpenAI fallback if apiKey set)\n * - provider='openai' → Embeddings (OpenAI)\n * - provider='onnx' → OnnxEmbeddingProvider (with optional OpenAI fallback if apiKey set)\n */\nexport function createEmbeddingProvider(cfg: EmbeddingConfig, onFallback?: (err: unknown) => void): EmbeddingProvider {\n const { provider, model, apiKey, models, dimensions, endpoint, batchSize, preferredProviders, retryIntervalMs } = cfg;\n const openaiApiModels = openAiEmbeddingApiModels(cfg);\n\n if (preferredProviders && preferredProviders.length > 1) {\n const chain: EmbeddingProvider[] = [];\n const labels: string[] = [];\n // When Google is in the chain with an OpenAI-only model name, we use 768 for Google; chain requires same dimensions for all.\n const googleInChainWithOpenAiModel =\n preferredProviders.includes(\"google\") && model && OPENAI_ONLY_EMBED_MODELS.has(model);\n const chainDimensions = googleInChainWithOpenAiModel ? GOOGLE_EMBED_DEFAULT_DIMENSIONS : dimensions;\n if (googleInChainWithOpenAiModel && dimensions && dimensions !== GOOGLE_EMBED_DEFAULT_DIMENSIONS) {\n pluginLogger.warn(\n `memory-hybrid: embedding chain includes Google with OpenAI model '${model}' — ` +\n `forcing dimensions to ${GOOGLE_EMBED_DEFAULT_DIMENSIONS} (Google default) instead of ${dimensions}. ` +\n `Set embedding.preferredProviders: [\"openai\"] to use ${dimensions}-dim OpenAI-only embeddings, ` +\n `or set embedding.dimensions: ${GOOGLE_EMBED_DEFAULT_DIMENSIONS} to acknowledge the Google chain dimensions.`,\n );\n }\n const ollamaModel =\n model && ![\"text-embedding-3-small\", \"text-embedding-3-large\", \"text-embedding-ada-002\"].includes(model)\n ? model\n : \"nomic-embed-text\";\n const googleModel = model && KNOWN_GOOGLE_EMBED_MODELS.has(model) ? model : GOOGLE_EMBED_DEFAULT_MODEL;\n const openaiChainModels = openAiEmbeddingApiModels(cfg, true);\n for (const name of preferredProviders) {\n if (name === \"ollama\") {\n try {\n chain.push(\n new OllamaEmbeddingProvider({ model: ollamaModel, dimensions: chainDimensions, endpoint, batchSize }),\n );\n labels.push(\"ollama\");\n } catch (err) {\n capturePluginError(err instanceof Error ? err : new Error(String(err)), {\n subsystem: \"embeddings\",\n operation: \"chain-build-ollama\",\n });\n }\n } else if (name === \"openai\" && apiKey) {\n try {\n const client = new OpenAI(openaiEmbeddingClientOpts(apiKey, endpoint));\n chain.push(\n new Embeddings(\n client,\n openaiChainModels,\n chainDimensions,\n batchSize,\n azureEmbeddingLogicalModelHint(cfg),\n omitEmbeddingDimensionsForAzure(cfg),\n ),\n );\n labels.push(\"openai\");\n } catch (err) {\n capturePluginError(err instanceof Error ? err : new Error(String(err)), {\n subsystem: \"embeddings\",\n operation: \"chain-build-openai\",\n });\n }\n } else if (name === \"google\" && cfg.googleApiKey && cfg.googleApiKey.length >= 10) {\n try {\n const client = new OpenAI({ apiKey: cfg.googleApiKey, baseURL: GOOGLE_EMBEDDING_BASE_URL });\n chain.push(new Embeddings(client, googleModel, chainDimensions, batchSize));\n labels.push(\"google\");\n } catch (err) {\n capturePluginError(err instanceof Error ? err : new Error(String(err)), {\n subsystem: \"embeddings\",\n operation: \"chain-build-google\",\n });\n }\n }\n }\n if (chain.length === 0) {\n throw new Error(\n \"embedding.preferredProviders: no provider could be built (check apiKey for openai/google, Ollama for ollama, distill.apiKey or llm.providers.google for Google).\",\n );\n }\n if (chain.length === 1) {\n return chain[0];\n }\n return new ChainEmbeddingProvider(chain, labels);\n }\n\n if (provider === \"ollama\") {\n const primary = new OllamaEmbeddingProvider({ model, dimensions, endpoint, batchSize });\n // Optional fallback to OpenAI when a key is provided\n if (apiKey) {\n const openaiClient = new OpenAI(openaiEmbeddingClientOpts(apiKey, endpoint));\n try {\n const fallback = new Embeddings(\n openaiClient,\n openAiEmbeddingApiModels(cfg, true),\n dimensions,\n batchSize,\n azureEmbeddingLogicalModelHint(cfg),\n omitEmbeddingDimensionsForAzure(cfg),\n );\n return new FallbackEmbeddingProvider(primary, fallback, onFallback, \"ollama\", \"openai\", retryIntervalMs);\n } catch (err) {\n // Fallback creation failed (e.g. Ollama dimensions exceed all OpenAI model limits).\n // Warn the user so they know their fallback isn't working.\n pluginLogger.warn(\n `memory-hybrid: Failed to create OpenAI fallback for Ollama provider: ${err instanceof Error ? err.message : String(err)}. Continuing with Ollama-only (no fallback).`,\n );\n return primary;\n }\n }\n return primary;\n }\n\n if (provider === \"openai\") {\n if (!apiKey) throw new Error(\"OpenAI embedding provider requires embedding.apiKey\");\n const openaiClient = new OpenAI(openaiEmbeddingClientOpts(apiKey, endpoint));\n return new Embeddings(\n openaiClient,\n openaiApiModels,\n dimensions,\n batchSize,\n azureEmbeddingLogicalModelHint(cfg),\n omitEmbeddingDimensionsForAzure(cfg),\n );\n }\n\n if (provider === \"google\") {\n if (!cfg.googleApiKey || cfg.googleApiKey.length < 10) {\n throw new Error(\"Google embedding provider requires distill.apiKey or llm.providers.google.apiKey.\");\n }\n const client = new OpenAI({ apiKey: cfg.googleApiKey, baseURL: GOOGLE_EMBEDDING_BASE_URL });\n // Use configured model only when it is a known Google embedding model; otherwise default.\n // When config has an OpenAI-only model name (e.g. text-embedding-3-large), use 768 dims so the API and vectors match.\n const googleEmbedModel = model && KNOWN_GOOGLE_EMBED_MODELS.has(model) ? model : GOOGLE_EMBED_DEFAULT_MODEL;\n const googleDimensions =\n model && OPENAI_ONLY_EMBED_MODELS.has(model) ? GOOGLE_EMBED_DEFAULT_DIMENSIONS : dimensions;\n return new Embeddings(client, googleEmbedModel, googleDimensions, batchSize);\n }\n\n if (provider === \"onnx\") {\n const primary = new OnnxEmbeddingProvider({ model, dimensions, batchSize });\n if (apiKey) {\n const openaiClient = new OpenAI(openaiEmbeddingClientOpts(apiKey, endpoint));\n try {\n const fallback = new Embeddings(\n openaiClient,\n openAiEmbeddingApiModels(cfg, true),\n dimensions,\n batchSize,\n azureEmbeddingLogicalModelHint(cfg),\n omitEmbeddingDimensionsForAzure(cfg),\n );\n const onSwitch = (err: unknown) => {\n if (isOnnxRuntimeMissingError(err)) {\n pluginLogger.warn(\"memory-hybrid: onnxruntime-node not installed; falling back to OpenAI embeddings.\");\n } else {\n pluginLogger.warn(\n `memory-hybrid: ONNX embeddings failed; falling back to OpenAI. ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n onFallback?.(err);\n };\n return new FallbackEmbeddingProvider(primary, fallback, onSwitch, \"onnx\", \"openai\", retryIntervalMs);\n } catch (err) {\n pluginLogger.warn(\n `memory-hybrid: Failed to create OpenAI fallback for ONNX provider: ${err instanceof Error ? err.message : String(err)}. Continuing with ONNX-only (no fallback).`,\n );\n return primary;\n }\n }\n return primary;\n }\n\n throw new Error(`Unknown embedding provider: '${provider as string}'. Valid options: openai, ollama, onnx, google.`);\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fallback-provider.js","names":[],"sources":["../../../services/embeddings/fallback-provider.ts"],"sourcesContent":["/**\n * FallbackEmbeddingProvider: tries primary provider, permanently switches to fallback on first failure.\n */\n\nimport { pluginLogger } from \"../../utils/logger.js\";\nimport { capturePluginError } from \"../error-reporter.js\";\nimport { shouldSuppressEmbeddingError } from \"./shared.js\";\nimport type { EmbeddingProvider } from \"./types.js\";\n\n/**\n * Wrapper that tries a primary provider and switches permanently to a fallback on first failure.\n * Useful for Ollama → OpenAI fallback when Ollama is temporarily unavailable.\n */\nexport class FallbackEmbeddingProvider implements EmbeddingProvider {\n private active: EmbeddingProvider;\n private readonly primary: EmbeddingProvider;\n private readonly fallback: EmbeddingProvider | null;\n private switched = false;\n private lastRetryAttempt = 0;\n private readonly retryIntervalMs: number;\n private readonly onSwitch?: (err: unknown) => void;\n private readonly primaryLabel: string;\n private readonly fallbackLabel: string;\n readonly dimensions: number;\n modelName: string;\n /** \"ollama\" when using primary, \"openai\" when using fallback (so logs reflect actual provider). */\n get activeProvider(): string {\n return this.switched ? this.fallbackLabel : this.primaryLabel;\n }\n\n constructor(\n primary: EmbeddingProvider,\n fallback: EmbeddingProvider | null,\n onSwitch?: (err: unknown) => void,\n primaryLabel = \"ollama\",\n fallbackLabel = \"openai\",\n retryIntervalMs = 60000,\n ) {\n if (!Number.isFinite(retryIntervalMs) || retryIntervalMs <= 0) {\n throw new Error(`FallbackEmbeddingProvider: retryIntervalMs must be a finite number > 0, got ${retryIntervalMs}`);\n }\n if (retryIntervalMs < 1000) {\n // Values below 1 s are valid for tests but will hammer the primary provider in production.\n pluginLogger.warn(\n `FallbackEmbeddingProvider: retryIntervalMs=${retryIntervalMs}ms is very low; values under 1000ms may cause excessive primary-probe traffic after a fallback switch.`,\n );\n }\n if (fallback && fallback.dimensions !== primary.dimensions) {\n throw new Error(\n `Primary (${primary.modelName}: ${primary.dimensions}d) and fallback ` +\n `(${fallback.modelName}: ${fallback.dimensions}d) must have matching dimensions`,\n );\n }\n this.active = primary;\n this.primary = primary;\n this.fallback = fallback;\n this.onSwitch = onSwitch;\n this.primaryLabel = primaryLabel;\n this.fallbackLabel = fallbackLabel;\n this.retryIntervalMs = retryIntervalMs;\n this.dimensions = primary.dimensions;\n this.modelName = primary.modelName;\n }\n\n /**\n * Attempt to return to the primary provider after a fallback switch.\n * Updates `switched`, `active`, and `modelName` on success.\n * Returns the result if primary recovered, or null if still failing (stay on fallback).\n */\n private async tryReturnToPrimary<T>(\n fn: (provider: EmbeddingProvider) => Promise<T>,\n phase: string,\n ): Promise<T | null> {\n this.lastRetryAttempt = Date.now();\n try {\n const result = await fn(this.primary);\n this.active = this.primary;\n this.switched = false;\n this.modelName = this.active.modelName;\n return result;\n } catch (err) {\n const asErr = err instanceof Error ? err : new Error(String(err));\n // Skip reporting config errors (404/403/401 — operator issues), 429 (rate limit), and circuit breaker open — not bugs (#329, #394, #385, #397, #458).\n if (!shouldSuppressEmbeddingError(asErr)) {\n capturePluginError(asErr, {\n subsystem: \"embeddings\",\n operation: \"fallback-retry-primary\",\n phase,\n });\n }\n // Primary still failing — continue using fallback\n return null;\n }\n }\n\n async embed(text: string): Promise<number[]> {\n if (!this.fallback) {\n return this.active.embed(text);\n }\n if (this.switched && Date.now() - this.lastRetryAttempt >= this.retryIntervalMs) {\n const recovered = await this.tryReturnToPrimary((p) => p.embed(text), \"embed\");\n if (recovered !== null) return recovered;\n }\n if (this.switched) {\n return this.active.embed(text);\n }\n try {\n return await this.active.embed(text);\n } catch (err) {\n const asErr = err instanceof Error ? err : new Error(String(err));\n // Skip reporting config errors (404/403/401 — operator issues), 429 (rate limit), and circuit breaker open — not bugs (#329, #394, #385, #397, #458).\n if (!shouldSuppressEmbeddingError(asErr)) {\n capturePluginError(asErr, {\n subsystem: \"embeddings\",\n operation: \"fallback-switch\",\n phase: \"embed\",\n });\n }\n this.onSwitch?.(err);\n this.active = this.fallback;\n this.switched = true;\n this.lastRetryAttempt = Date.now();\n this.modelName = this.active.modelName;\n return this.active.embed(text);\n }\n }\n\n async embedBatch(texts: string[]): Promise<number[][]> {\n if (!this.fallback) {\n return this.active.embedBatch(texts);\n }\n if (this.switched && Date.now() - this.lastRetryAttempt >= this.retryIntervalMs) {\n const recovered = await this.tryReturnToPrimary((p) => p.embedBatch(texts), \"embedBatch\");\n if (recovered !== null) return recovered;\n }\n if (this.switched) {\n return this.active.embedBatch(texts);\n }\n try {\n return await this.active.embedBatch(texts);\n } catch (err) {\n const asErr = err instanceof Error ? err : new Error(String(err));\n // Skip reporting config errors (404/403/401 — operator issues), 429 (rate limit), and circuit breaker open — not bugs (#329, #394, #385, #397, #458).\n if (!shouldSuppressEmbeddingError(asErr)) {\n capturePluginError(asErr, {\n subsystem: \"embeddings\",\n operation: \"fallback-switch\",\n phase: \"embedBatch\",\n });\n }\n this.onSwitch?.(err);\n this.active = this.fallback;\n this.switched = true;\n this.lastRetryAttempt = Date.now();\n this.modelName = this.active.modelName;\n return this.active.embedBatch(texts);\n }\n }\n}\n"],"mappings":";;;;;;;;;;;AAaA,IAAa,4BAAb,MAAoE;CAClE;CACA;CACA;CACA,WAAmB;CACnB,mBAA2B;CAC3B;CACA;CACA;CACA;CACA;CACA;;CAEA,IAAI,iBAAyB;EAC3B,OAAO,KAAK,WAAW,KAAK,gBAAgB,KAAK
|
|
1
|
+
{"version":3,"file":"fallback-provider.js","names":[],"sources":["../../../services/embeddings/fallback-provider.ts"],"sourcesContent":["/**\n * FallbackEmbeddingProvider: tries primary provider, permanently switches to fallback on first failure.\n */\n\nimport { pluginLogger } from \"../../utils/logger.js\";\nimport { capturePluginError } from \"../error-reporter.js\";\nimport { shouldSuppressEmbeddingError } from \"./shared.js\";\nimport type { EmbeddingProvider } from \"./types.js\";\n\n/**\n * Wrapper that tries a primary provider and switches permanently to a fallback on first failure.\n * Useful for Ollama → OpenAI fallback when Ollama is temporarily unavailable.\n */\nexport class FallbackEmbeddingProvider implements EmbeddingProvider {\n private active: EmbeddingProvider;\n private readonly primary: EmbeddingProvider;\n private readonly fallback: EmbeddingProvider | null;\n private switched = false;\n private lastRetryAttempt = 0;\n private readonly retryIntervalMs: number;\n private readonly onSwitch?: (err: unknown) => void;\n private readonly primaryLabel: string;\n private readonly fallbackLabel: string;\n readonly dimensions: number;\n modelName: string;\n /** \"ollama\" when using primary, \"openai\" when using fallback (so logs reflect actual provider). */\n get activeProvider(): string {\n return this.switched ? this.fallbackLabel : this.primaryLabel;\n }\n\n constructor(\n primary: EmbeddingProvider,\n fallback: EmbeddingProvider | null,\n onSwitch?: (err: unknown) => void,\n primaryLabel = \"ollama\",\n fallbackLabel = \"openai\",\n retryIntervalMs = 60000,\n ) {\n if (!Number.isFinite(retryIntervalMs) || retryIntervalMs <= 0) {\n throw new Error(`FallbackEmbeddingProvider: retryIntervalMs must be a finite number > 0, got ${retryIntervalMs}`);\n }\n if (retryIntervalMs < 1000) {\n // Values below 1 s are valid for tests but will hammer the primary provider in production.\n pluginLogger.warn(\n `FallbackEmbeddingProvider: retryIntervalMs=${retryIntervalMs}ms is very low; values under 1000ms may cause excessive primary-probe traffic after a fallback switch.`,\n );\n }\n if (fallback && fallback.dimensions !== primary.dimensions) {\n throw new Error(\n `Primary (${primary.modelName}: ${primary.dimensions}d) and fallback ` +\n `(${fallback.modelName}: ${fallback.dimensions}d) must have matching dimensions`,\n );\n }\n this.active = primary;\n this.primary = primary;\n this.fallback = fallback;\n this.onSwitch = onSwitch;\n this.primaryLabel = primaryLabel;\n this.fallbackLabel = fallbackLabel;\n this.retryIntervalMs = retryIntervalMs;\n this.dimensions = primary.dimensions;\n this.modelName = primary.modelName;\n }\n\n /**\n * Attempt to return to the primary provider after a fallback switch.\n * Updates `switched`, `active`, and `modelName` on success.\n * Returns the result if primary recovered, or null if still failing (stay on fallback).\n */\n private async tryReturnToPrimary<T>(\n fn: (provider: EmbeddingProvider) => Promise<T>,\n phase: string,\n ): Promise<T | null> {\n this.lastRetryAttempt = Date.now();\n try {\n const result = await fn(this.primary);\n this.active = this.primary;\n this.switched = false;\n this.modelName = this.active.modelName;\n return result;\n } catch (err) {\n const asErr = err instanceof Error ? err : new Error(String(err));\n // Skip reporting config errors (404/403/401 — operator issues), 429 (rate limit), and circuit breaker open — not bugs (#329, #394, #385, #397, #458).\n if (!shouldSuppressEmbeddingError(asErr)) {\n capturePluginError(asErr, {\n subsystem: \"embeddings\",\n operation: \"fallback-retry-primary\",\n phase,\n });\n }\n // Primary still failing — continue using fallback\n return null;\n }\n }\n\n async embed(text: string): Promise<number[]> {\n if (!this.fallback) {\n return this.active.embed(text);\n }\n if (this.switched && Date.now() - this.lastRetryAttempt >= this.retryIntervalMs) {\n const recovered = await this.tryReturnToPrimary((p) => p.embed(text), \"embed\");\n if (recovered !== null) return recovered;\n }\n if (this.switched) {\n return this.active.embed(text);\n }\n try {\n return await this.active.embed(text);\n } catch (err) {\n const asErr = err instanceof Error ? err : new Error(String(err));\n // Skip reporting config errors (404/403/401 — operator issues), 429 (rate limit), and circuit breaker open — not bugs (#329, #394, #385, #397, #458).\n if (!shouldSuppressEmbeddingError(asErr)) {\n capturePluginError(asErr, {\n subsystem: \"embeddings\",\n operation: \"fallback-switch\",\n phase: \"embed\",\n });\n }\n this.onSwitch?.(err);\n this.active = this.fallback;\n this.switched = true;\n this.lastRetryAttempt = Date.now();\n this.modelName = this.active.modelName;\n return this.active.embed(text);\n }\n }\n\n async embedBatch(texts: string[]): Promise<number[][]> {\n if (!this.fallback) {\n return this.active.embedBatch(texts);\n }\n if (this.switched && Date.now() - this.lastRetryAttempt >= this.retryIntervalMs) {\n const recovered = await this.tryReturnToPrimary((p) => p.embedBatch(texts), \"embedBatch\");\n if (recovered !== null) return recovered;\n }\n if (this.switched) {\n return this.active.embedBatch(texts);\n }\n try {\n return await this.active.embedBatch(texts);\n } catch (err) {\n const asErr = err instanceof Error ? err : new Error(String(err));\n // Skip reporting config errors (404/403/401 — operator issues), 429 (rate limit), and circuit breaker open — not bugs (#329, #394, #385, #397, #458).\n if (!shouldSuppressEmbeddingError(asErr)) {\n capturePluginError(asErr, {\n subsystem: \"embeddings\",\n operation: \"fallback-switch\",\n phase: \"embedBatch\",\n });\n }\n this.onSwitch?.(err);\n this.active = this.fallback;\n this.switched = true;\n this.lastRetryAttempt = Date.now();\n this.modelName = this.active.modelName;\n return this.active.embedBatch(texts);\n }\n }\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ollama-provider.js","names":[],"sources":["../../../services/embeddings/ollama-provider.ts"],"sourcesContent":["/**\n * Ollama local embedding provider with per-endpoint circuit breaker.\n */\n\nimport { OLLAMA_COOLDOWN_MS, OLLAMA_MAX_FAILS } from \"../../utils/constants.js\";\nimport { pluginLogger } from \"../../utils/logger.js\";\nimport type { EmbeddingProvider } from \"./types.js\";\n\n/**\n * Module-level circuit breaker state keyed by Ollama endpoint URL.\n * Shared across all OllamaEmbeddingProvider instances so that a failure on\n * one instance is visible to new instances pointing at the same endpoint,\n * while endpoints at different base URLs remain independent.\n */\nconst _ollamaCircuitByEndpoint = new Map<string, { failCount: number; disabledUntil: number }>();\n\nfunction _getOllamaCircuit(endpoint: string): { failCount: number; disabledUntil: number } {\n if (!_ollamaCircuitByEndpoint.has(endpoint)) {\n _ollamaCircuitByEndpoint.set(endpoint, { failCount: 0, disabledUntil: 0 });\n }\n return _ollamaCircuitByEndpoint.get(endpoint)!;\n}\n\n/**\n * Reset the Ollama circuit breaker state for a given endpoint (or all endpoints if omitted).\n * Intended for use in tests only — do not call in production code.\n */\nexport function _resetOllamaCircuitBreakerForTesting(endpoint?: string): void {\n if (endpoint) {\n _ollamaCircuitByEndpoint.delete(endpoint);\n } else {\n _ollamaCircuitByEndpoint.clear();\n }\n}\n\n/**\n * Ollama-based embedding provider.\n * Calls Ollama REST API (POST /api/embed) — no external API key required.\n */\nexport class OllamaEmbeddingProvider implements EmbeddingProvider {\n readonly dimensions: number;\n readonly modelName: string;\n private readonly endpoint: string;\n private readonly batchSize: number;\n\n constructor(opts: { model: string; dimensions: number; endpoint?: string; batchSize?: number }) {\n this.modelName = opts.model;\n this.dimensions = opts.dimensions;\n this.endpoint = (opts.endpoint ?? \"http://localhost:11434\").replace(/\\/$/, \"\");\n this.batchSize = opts.batchSize || 50;\n }\n\n async embed(text: string): Promise<number[]> {\n const results = await this.embedBatch([text]);\n if (results.length === 0) {\n throw new Error(\"Ollama embed returned empty results for single text\");\n }\n return results[0];\n }\n\n /** Maximum characters per input text sent to Ollama (~2000 tokens for most models). */\n private static readonly MAX_INPUT_CHARS = 8000;\n\n async embedBatch(texts: string[]): Promise<number[][]> {\n // Circuit breaker: shared per endpoint so all instances pointing at the same URL are gated together.\n // This prevents one bad endpoint from being retried across separately-constructed instances,\n // while leaving providers at different base URLs unaffected.\n const circuit = _getOllamaCircuit(this.endpoint);\n if (Date.now() < circuit.disabledUntil) {\n throw new Error(\n `Ollama circuit breaker open — disabled until ${new Date(circuit.disabledUntil).toISOString()} (endpoint: ${this.endpoint})`,\n );\n }\n\n const allResults: number[][] = [];\n for (let i = 0; i < texts.length; i += this.batchSize) {\n const batch = texts.slice(i, i + this.batchSize).map((t) => {\n if (t.length > OllamaEmbeddingProvider.MAX_INPUT_CHARS) {\n pluginLogger.warn(\n `memory-hybrid: Truncating embedding input from ${t.length} to ${OllamaEmbeddingProvider.MAX_INPUT_CHARS} chars for ${this.modelName}`,\n );\n return t.slice(0, OllamaEmbeddingProvider.MAX_INPUT_CHARS);\n }\n return t;\n });\n let resp: Response;\n try {\n resp = await fetch(`${this.endpoint}/api/embed`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ model: this.modelName, input: batch }),\n });\n } catch (err) {\n // Connection failure — update shared circuit breaker state for this endpoint\n circuit.failCount++;\n if (circuit.failCount >= OLLAMA_MAX_FAILS) {\n circuit.disabledUntil = Date.now() + OLLAMA_COOLDOWN_MS;\n pluginLogger.warn(\n `memory-hybrid: Ollama circuit breaker open — disabling endpoint ${this.endpoint} for 5min after ${circuit.failCount} failures`,\n );\n }\n throw new Error(`Ollama connection failed (${this.endpoint}): ${err}`);\n }\n if (!resp.ok) {\n const body = await resp.text().catch(() => \"\");\n const errMsg = `Ollama embed failed: HTTP ${resp.status} ${resp.statusText}${body ? ` — ${body.slice(0, 200)}` : \"\"}`;\n // OOM: trip circuit breaker immediately — retrying the same model won't free memory.\n const isOOM =\n body.toLowerCase().includes(\"model requires more system memory\") ||\n body.toLowerCase().includes(\"not enough memory to load\") ||\n /\\bmodel\\s+requires\\s+[\\d.]+\\s*gib/i.test(body) ||\n /\\boom:/i.test(body);\n if (isOOM) {\n circuit.disabledUntil = Date.now() + OLLAMA_COOLDOWN_MS;\n circuit.failCount = OLLAMA_MAX_FAILS;\n pluginLogger.warn(\n `memory-hybrid: Ollama model OOM (${this.modelName}) — model requires more memory than available. Circuit breaker tripped; disabling endpoint ${this.endpoint} for 5min. Consider using a smaller model or configuring a cloud embedding fallback.`,\n );\n }\n throw new Error(errMsg);\n }\n const data = (await resp.json()) as { embeddings: number[][] };\n if (!Array.isArray(data.embeddings)) {\n throw new Error(`Ollama embed response missing 'embeddings' array`);\n }\n if (data.embeddings.length === 0) {\n throw new Error(`Ollama embed returned empty 'embeddings' array (expected ${batch.length})`);\n }\n if (data.embeddings.length !== batch.length) {\n throw new Error(`Ollama embed returned ${data.embeddings.length} embeddings for ${batch.length} inputs`);\n }\n allResults.push(...data.embeddings);\n }\n // Successful call — reset circuit breaker for this endpoint\n circuit.failCount = 0;\n circuit.disabledUntil = 0;\n return allResults;\n }\n}\n"],"mappings":";;;;;;;;;;;;AAcA,MAAM,2CAA2B,IAAI,
|
|
1
|
+
{"version":3,"file":"ollama-provider.js","names":[],"sources":["../../../services/embeddings/ollama-provider.ts"],"sourcesContent":["/**\n * Ollama local embedding provider with per-endpoint circuit breaker.\n */\n\nimport { OLLAMA_COOLDOWN_MS, OLLAMA_MAX_FAILS } from \"../../utils/constants.js\";\nimport { pluginLogger } from \"../../utils/logger.js\";\nimport type { EmbeddingProvider } from \"./types.js\";\n\n/**\n * Module-level circuit breaker state keyed by Ollama endpoint URL.\n * Shared across all OllamaEmbeddingProvider instances so that a failure on\n * one instance is visible to new instances pointing at the same endpoint,\n * while endpoints at different base URLs remain independent.\n */\nconst _ollamaCircuitByEndpoint = new Map<string, { failCount: number; disabledUntil: number }>();\n\nfunction _getOllamaCircuit(endpoint: string): { failCount: number; disabledUntil: number } {\n if (!_ollamaCircuitByEndpoint.has(endpoint)) {\n _ollamaCircuitByEndpoint.set(endpoint, { failCount: 0, disabledUntil: 0 });\n }\n return _ollamaCircuitByEndpoint.get(endpoint)!;\n}\n\n/**\n * Reset the Ollama circuit breaker state for a given endpoint (or all endpoints if omitted).\n * Intended for use in tests only — do not call in production code.\n */\nexport function _resetOllamaCircuitBreakerForTesting(endpoint?: string): void {\n if (endpoint) {\n _ollamaCircuitByEndpoint.delete(endpoint);\n } else {\n _ollamaCircuitByEndpoint.clear();\n }\n}\n\n/**\n * Ollama-based embedding provider.\n * Calls Ollama REST API (POST /api/embed) — no external API key required.\n */\nexport class OllamaEmbeddingProvider implements EmbeddingProvider {\n readonly dimensions: number;\n readonly modelName: string;\n private readonly endpoint: string;\n private readonly batchSize: number;\n\n constructor(opts: { model: string; dimensions: number; endpoint?: string; batchSize?: number }) {\n this.modelName = opts.model;\n this.dimensions = opts.dimensions;\n this.endpoint = (opts.endpoint ?? \"http://localhost:11434\").replace(/\\/$/, \"\");\n this.batchSize = opts.batchSize || 50;\n }\n\n async embed(text: string): Promise<number[]> {\n const results = await this.embedBatch([text]);\n if (results.length === 0) {\n throw new Error(\"Ollama embed returned empty results for single text\");\n }\n return results[0];\n }\n\n /** Maximum characters per input text sent to Ollama (~2000 tokens for most models). */\n private static readonly MAX_INPUT_CHARS = 8000;\n\n async embedBatch(texts: string[]): Promise<number[][]> {\n // Circuit breaker: shared per endpoint so all instances pointing at the same URL are gated together.\n // This prevents one bad endpoint from being retried across separately-constructed instances,\n // while leaving providers at different base URLs unaffected.\n const circuit = _getOllamaCircuit(this.endpoint);\n if (Date.now() < circuit.disabledUntil) {\n throw new Error(\n `Ollama circuit breaker open — disabled until ${new Date(circuit.disabledUntil).toISOString()} (endpoint: ${this.endpoint})`,\n );\n }\n\n const allResults: number[][] = [];\n for (let i = 0; i < texts.length; i += this.batchSize) {\n const batch = texts.slice(i, i + this.batchSize).map((t) => {\n if (t.length > OllamaEmbeddingProvider.MAX_INPUT_CHARS) {\n pluginLogger.warn(\n `memory-hybrid: Truncating embedding input from ${t.length} to ${OllamaEmbeddingProvider.MAX_INPUT_CHARS} chars for ${this.modelName}`,\n );\n return t.slice(0, OllamaEmbeddingProvider.MAX_INPUT_CHARS);\n }\n return t;\n });\n let resp: Response;\n try {\n resp = await fetch(`${this.endpoint}/api/embed`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ model: this.modelName, input: batch }),\n });\n } catch (err) {\n // Connection failure — update shared circuit breaker state for this endpoint\n circuit.failCount++;\n if (circuit.failCount >= OLLAMA_MAX_FAILS) {\n circuit.disabledUntil = Date.now() + OLLAMA_COOLDOWN_MS;\n pluginLogger.warn(\n `memory-hybrid: Ollama circuit breaker open — disabling endpoint ${this.endpoint} for 5min after ${circuit.failCount} failures`,\n );\n }\n throw new Error(`Ollama connection failed (${this.endpoint}): ${err}`);\n }\n if (!resp.ok) {\n const body = await resp.text().catch(() => \"\");\n const errMsg = `Ollama embed failed: HTTP ${resp.status} ${resp.statusText}${body ? ` — ${body.slice(0, 200)}` : \"\"}`;\n // OOM: trip circuit breaker immediately — retrying the same model won't free memory.\n const isOOM =\n body.toLowerCase().includes(\"model requires more system memory\") ||\n body.toLowerCase().includes(\"not enough memory to load\") ||\n /\\bmodel\\s+requires\\s+[\\d.]+\\s*gib/i.test(body) ||\n /\\boom:/i.test(body);\n if (isOOM) {\n circuit.disabledUntil = Date.now() + OLLAMA_COOLDOWN_MS;\n circuit.failCount = OLLAMA_MAX_FAILS;\n pluginLogger.warn(\n `memory-hybrid: Ollama model OOM (${this.modelName}) — model requires more memory than available. Circuit breaker tripped; disabling endpoint ${this.endpoint} for 5min. Consider using a smaller model or configuring a cloud embedding fallback.`,\n );\n }\n throw new Error(errMsg);\n }\n const data = (await resp.json()) as { embeddings: number[][] };\n if (!Array.isArray(data.embeddings)) {\n throw new Error(`Ollama embed response missing 'embeddings' array`);\n }\n if (data.embeddings.length === 0) {\n throw new Error(`Ollama embed returned empty 'embeddings' array (expected ${batch.length})`);\n }\n if (data.embeddings.length !== batch.length) {\n throw new Error(`Ollama embed returned ${data.embeddings.length} embeddings for ${batch.length} inputs`);\n }\n allResults.push(...data.embeddings);\n }\n // Successful call — reset circuit breaker for this endpoint\n circuit.failCount = 0;\n circuit.disabledUntil = 0;\n return allResults;\n }\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"onnx-provider.js","names":["fs"],"sources":["../../../services/embeddings/onnx-provider.ts"],"sourcesContent":["/**\n * ONNX local embedding provider.\n */\n\nimport { promises as fs } from \"node:fs\";\nimport { createWriteStream } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { dirname, join, resolve } from \"node:path\";\nimport { Readable, Transform } from \"node:stream\";\nimport { pipeline } from \"node:stream/promises\";\nimport type { EmbeddingProvider } from \"./types.js\";\n\n// ---------------------------------------------------------------------------\n// ONNX type shims (loaded dynamically at runtime; package is optional).\n// ---------------------------------------------------------------------------\n\ninterface OnnxTensor {\n data: Float32Array | Int32Array | BigInt64Array;\n dims: readonly number[];\n}\ninterface OnnxInferenceSession {\n readonly inputNames: readonly string[];\n run(feeds: Record<string, OnnxTensor>): Promise<Record<string, OnnxTensor>>;\n}\ninterface OnnxRuntime {\n InferenceSession: { create(modelPath: string): Promise<OnnxInferenceSession> };\n Tensor: new (type: string, data: BigInt64Array, dims: number[]) => OnnxTensor;\n}\ntype OnnxRuntimeLoader = () => Promise<OnnxRuntime>;\n\nconst DEFAULT_ONNX_CACHE_DIR = join(homedir(), \".cache\", \"openclaw\", \"onnx-embeddings\");\nconst DEFAULT_ONNX_MAX_SEQ_LEN = 256;\nexport const ONNX_RUNTIME_LOAD_TIMEOUT_MS = 30_000;\n\nconst ONNX_MODEL_SPECS: Record<string, { repo: string; modelFile: string; vocabFileCandidates: string[] }> = {\n \"all-MiniLM-L6-v2\": {\n repo: \"sentence-transformers/all-MiniLM-L6-v2\",\n modelFile: \"onnx/model.onnx\",\n vocabFileCandidates: [\"vocab.txt\", \"tokenizer/vocab.txt\"],\n },\n \"bge-small-en-v1.5\": {\n repo: \"BAAI/bge-small-en-v1.5\",\n modelFile: \"onnx/model.onnx\",\n vocabFileCandidates: [\"vocab.txt\", \"tokenizer/vocab.txt\"],\n },\n};\n\nclass OnnxRuntimeMissingError extends Error {\n readonly code = \"ONNX_RUNTIME_MISSING\";\n constructor(message: string) {\n super(message);\n this.name = \"OnnxRuntimeMissingError\";\n }\n}\n\nclass OnnxRuntimeLoadTimeoutError extends Error {\n readonly code = \"ONNX_RUNTIME_LOAD_TIMEOUT\";\n constructor(timeoutMs: number) {\n super(`Timed out loading onnxruntime-node after ${timeoutMs}ms.`);\n this.name = \"OnnxRuntimeLoadTimeoutError\";\n }\n}\n\nexport function isOnnxRuntimeMissingError(err: unknown): err is OnnxRuntimeMissingError {\n return (\n err instanceof OnnxRuntimeMissingError ||\n (err instanceof Error && (err as Error & { code?: string }).code === \"ONNX_RUNTIME_MISSING\")\n );\n}\n\nconst defaultOnnxRuntimeLoader: OnnxRuntimeLoader = () =>\n import(\"onnxruntime-node\") as Promise<unknown> as Promise<OnnxRuntime>;\nlet onnxRuntimeLoader: OnnxRuntimeLoader = defaultOnnxRuntimeLoader;\n\n/** @internal Test hook: override ONNX runtime loader. */\nexport function __setOnnxRuntimeLoaderForTests(loader: OnnxRuntimeLoader | null): void {\n onnxRuntimeLoader = loader ?? defaultOnnxRuntimeLoader;\n}\n\nasync function loadOnnxRuntime(): Promise<OnnxRuntime> {\n let timeoutId: ReturnType<typeof setTimeout> | undefined;\n try {\n const runtime = await Promise.race([\n onnxRuntimeLoader(),\n new Promise<never>((_, reject) => {\n timeoutId = setTimeout(\n () => reject(new OnnxRuntimeLoadTimeoutError(ONNX_RUNTIME_LOAD_TIMEOUT_MS)),\n ONNX_RUNTIME_LOAD_TIMEOUT_MS,\n );\n }),\n ]);\n if (timeoutId) {\n clearTimeout(timeoutId);\n timeoutId = undefined;\n }\n return runtime;\n } catch (err) {\n if (err instanceof OnnxRuntimeLoadTimeoutError) {\n throw err;\n }\n throw new OnnxRuntimeMissingError(\n \"onnxruntime-node is not installed. Install it to use provider='onnx' (e.g. npm i onnxruntime-node).\",\n );\n } finally {\n if (timeoutId) {\n clearTimeout(timeoutId);\n }\n }\n}\n\nasync function fileExists(path: string): Promise<boolean> {\n try {\n await fs.access(path);\n return true;\n } catch {\n return false;\n }\n}\n\nfunction sanitizeRepoDir(repo: string): string {\n return repo.replace(/[^\\w.-]+/g, \"__\");\n}\n\nfunction splitRepoAndRevision(input: string): { repo: string; revision: string } {\n const at = input.lastIndexOf(\"@\");\n if (at > 0 && at < input.length - 1) {\n return { repo: input.slice(0, at), revision: input.slice(at + 1) };\n }\n return { repo: input, revision: \"main\" };\n}\n\nconst ONNX_DOWNLOAD_TIMEOUT_MS = 120_000;\nconst ONNX_DOWNLOAD_MAX_BYTES = 500 * 1024 * 1024; // 500 MiB\n\nasync function downloadFile(url: string, destPath: string): Promise<void> {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), ONNX_DOWNLOAD_TIMEOUT_MS);\n const resp = await fetch(url, { signal: controller.signal });\n clearTimeout(timeoutId);\n if (!resp.ok || !resp.body) {\n const body = await resp.text().catch(() => \"\");\n throw new Error(\n `Failed to download ${url}: HTTP ${resp.status} ${resp.statusText}${body ? ` — ${body.slice(0, 200)}` : \"\"}`,\n );\n }\n const contentLength = resp.headers.get(\"content-length\");\n if (contentLength) {\n const len = Number.parseInt(contentLength, 10);\n if (!Number.isNaN(len) && len > ONNX_DOWNLOAD_MAX_BYTES) {\n throw new Error(`Refusing to download ${url}: size ${len} exceeds limit ${ONNX_DOWNLOAD_MAX_BYTES}`);\n }\n }\n await fs.mkdir(dirname(destPath), { recursive: true });\n const tempPath = `${destPath}.tmp`;\n const stream = Readable.fromWeb(resp.body as import(\"stream/web\").ReadableStream<Uint8Array>);\n try {\n let bytesWritten = 0;\n const countStream = new Transform({\n transform(chunk: Buffer, _enc, cb) {\n bytesWritten += chunk.length;\n if (bytesWritten > ONNX_DOWNLOAD_MAX_BYTES) {\n cb(new Error(`Download size exceeds limit ${ONNX_DOWNLOAD_MAX_BYTES}`));\n return;\n }\n cb(null, chunk);\n },\n });\n await pipeline(stream, countStream, createWriteStream(tempPath));\n await fs.rename(tempPath, destPath);\n } catch (err) {\n await fs.unlink(tempPath).catch(() => {});\n throw err;\n }\n}\n\nasync function resolveOnnxModelFiles(\n model: string,\n opts?: { cacheDir?: string; modelPath?: string; vocabPath?: string },\n): Promise<{ modelPath: string; vocabPath: string }> {\n const cacheDir = opts?.cacheDir ?? DEFAULT_ONNX_CACHE_DIR;\n if (opts?.modelPath) {\n const modelPath = resolve(opts.modelPath);\n if (!(await fileExists(modelPath))) {\n throw new Error(`ONNX model file not found at ${modelPath}`);\n }\n const vocabPath = opts?.vocabPath ? resolve(opts.vocabPath) : join(dirname(modelPath), \"vocab.txt\");\n if (!(await fileExists(vocabPath))) {\n throw new Error(`Tokenizer vocab.txt not found at ${vocabPath}`);\n }\n return { modelPath, vocabPath };\n }\n\n const resolvedModelPath = resolve(model);\n if (model.endsWith(\".onnx\") && (await fileExists(resolvedModelPath))) {\n const vocabPath = opts?.vocabPath ? resolve(opts.vocabPath) : join(dirname(resolvedModelPath), \"vocab.txt\");\n if (!(await fileExists(vocabPath))) {\n throw new Error(`Tokenizer vocab.txt not found at ${vocabPath}`);\n }\n return { modelPath: resolvedModelPath, vocabPath };\n }\n\n const spec = ONNX_MODEL_SPECS[model];\n const { repo, revision } = splitRepoAndRevision(spec?.repo ?? model);\n const modelFile = spec?.modelFile ?? \"onnx/model.onnx\";\n const vocabCandidates = spec?.vocabFileCandidates ?? [\"vocab.txt\", \"tokenizer/vocab.txt\"];\n const repoDir = join(cacheDir, sanitizeRepoDir(repo), revision);\n const modelPath = join(repoDir, modelFile);\n if (!(await fileExists(modelPath))) {\n const modelUrl = `https://huggingface.co/${repo}/resolve/${revision}/${modelFile}`;\n await downloadFile(modelUrl, modelPath);\n }\n let vocabPath = opts?.vocabPath ? resolve(opts.vocabPath) : \"\";\n if (vocabPath && !(await fileExists(vocabPath))) {\n throw new Error(`Tokenizer vocab.txt not found at ${vocabPath}`);\n }\n if (!vocabPath) {\n for (const candidate of vocabCandidates) {\n const candidatePath = join(repoDir, candidate);\n if (await fileExists(candidatePath)) {\n vocabPath = candidatePath;\n break;\n }\n const vocabUrl = `https://huggingface.co/${repo}/resolve/${revision}/${candidate}`;\n try {\n await downloadFile(vocabUrl, candidatePath);\n vocabPath = candidatePath;\n break;\n } catch {}\n }\n }\n if (!vocabPath) {\n throw new Error(`Unable to locate or download vocab.txt for model '${model}'.`);\n }\n return { modelPath, vocabPath };\n}\n\n// ---------------------------------------------------------------------------\n// Tokenizer helpers\n// ---------------------------------------------------------------------------\n\nfunction normalizeText(text: string): string {\n return text.normalize(\"NFD\").replace(/[\\u0300-\\u036f]/g, \"\");\n}\n\nfunction isPunctuation(char: string): boolean {\n const code = char.charCodeAt(0);\n return (\n (code >= 33 && code <= 47) ||\n (code >= 58 && code <= 64) ||\n (code >= 91 && code <= 96) ||\n (code >= 123 && code <= 126)\n );\n}\n\nfunction basicTokenize(text: string): string[] {\n const normalized = normalizeText(text).toLowerCase();\n const tokens: string[] = [];\n let current = \"\";\n for (const ch of normalized) {\n if (/\\s/.test(ch)) {\n if (current) {\n tokens.push(current);\n current = \"\";\n }\n continue;\n }\n if (isPunctuation(ch)) {\n if (current) {\n tokens.push(current);\n current = \"\";\n }\n tokens.push(ch);\n continue;\n }\n current += ch;\n }\n if (current) tokens.push(current);\n return tokens;\n}\n\nfunction wordPieceTokenize(token: string, vocab: Map<string, number>, unkToken: string, maxChars = 100): string[] {\n if (token.length > maxChars) return [unkToken];\n if (vocab.has(token)) return [token];\n const chars = token.split(\"\");\n const pieces: string[] = [];\n let start = 0;\n while (start < chars.length) {\n let end = chars.length;\n let found = \"\";\n while (start < end) {\n const substr = chars.slice(start, end).join(\"\");\n const candidate = start === 0 ? substr : `##${substr}`;\n if (vocab.has(candidate)) {\n found = candidate;\n break;\n }\n end -= 1;\n }\n if (!found) {\n return [unkToken];\n }\n pieces.push(found);\n start = end;\n }\n return pieces;\n}\n\ntype TokenizedInput = { inputIds: number[]; attentionMask: number[]; tokenTypeIds: number[] };\n\nclass WordPieceTokenizer {\n private vocab: Map<string, number>;\n private readonly clsToken: string;\n private readonly sepToken: string;\n private readonly padToken: string;\n private readonly unkToken: string;\n\n constructor(vocab: Map<string, number>) {\n this.vocab = vocab;\n this.clsToken = \"[CLS]\";\n this.sepToken = \"[SEP]\";\n this.padToken = \"[PAD]\";\n this.unkToken = \"[UNK]\";\n for (const tok of [this.clsToken, this.sepToken, this.padToken, this.unkToken]) {\n if (!this.vocab.has(tok)) {\n throw new Error(`Tokenizer vocab missing required token ${tok}`);\n }\n }\n }\n\n encode(text: string, maxLen: number): TokenizedInput {\n const tokens = basicTokenize(text).flatMap((t) => wordPieceTokenize(t, this.vocab, this.unkToken));\n const maxTokens = Math.max(0, maxLen - 2);\n const trimmed = tokens.length > maxTokens ? tokens.slice(0, maxTokens) : tokens;\n const finalTokens = [this.clsToken, ...trimmed, this.sepToken];\n const inputIds = finalTokens.map((t) => this.vocab.get(t) ?? this.vocab.get(this.unkToken)!);\n const attentionMask = new Array(finalTokens.length).fill(1);\n const tokenTypeIds = new Array(finalTokens.length).fill(0);\n return { inputIds, attentionMask, tokenTypeIds };\n }\n\n getPadTokenId(): number {\n return this.vocab.get(this.padToken)!;\n }\n}\n\nasync function loadVocab(vocabPath: string): Promise<Map<string, number>> {\n const content = await fs.readFile(vocabPath, \"utf-8\");\n const vocab = new Map<string, number>();\n const lines = content.split(/\\r?\\n/);\n for (let i = 0; i < lines.length; i++) {\n const token = lines[i].trim();\n if (!token) continue;\n vocab.set(token, i);\n }\n return vocab;\n}\n\nfunction l2Normalize(vec: Float32Array): Float32Array {\n let sum = 0;\n for (const v of vec) sum += v * v;\n const norm = Math.sqrt(sum);\n if (!norm) return vec;\n const out = new Float32Array(vec.length);\n for (let i = 0; i < vec.length; i++) out[i] = vec[i] / norm;\n return out;\n}\n\nfunction meanPool(\n lastHidden: Float32Array,\n attentionMask: BigInt64Array,\n batch: number,\n seq: number,\n hidden: number,\n): Float32Array[] {\n const outputs: Float32Array[] = [];\n let offset = 0;\n for (let b = 0; b < batch; b++) {\n const out = new Float32Array(hidden);\n let count = 0;\n for (let s = 0; s < seq; s++) {\n const mask = attentionMask[b * seq + s];\n if (mask === 0n) {\n offset += hidden;\n continue;\n }\n for (let h = 0; h < hidden; h++) {\n out[h] += lastHidden[offset + h];\n }\n count++;\n offset += hidden;\n }\n if (count > 0) {\n for (let h = 0; h < hidden; h++) out[h] /= count;\n }\n outputs.push(out);\n }\n return outputs;\n}\n\n// ---------------------------------------------------------------------------\n// OnnxEmbeddingProvider\n// ---------------------------------------------------------------------------\n\nexport class OnnxEmbeddingProvider implements EmbeddingProvider {\n readonly dimensions: number;\n readonly modelName: string;\n private readonly batchSize: number;\n private readonly maxSeqLength: number;\n private readonly cacheDir?: string;\n private readonly modelPath?: string;\n private readonly vocabPath?: string;\n private session?: OnnxInferenceSession;\n private tokenizer?: WordPieceTokenizer;\n private ort?: OnnxRuntime;\n private ready?: Promise<void>;\n\n constructor(opts: {\n model: string;\n dimensions: number;\n batchSize?: number;\n maxSeqLength?: number;\n cacheDir?: string;\n modelPath?: string;\n vocabPath?: string;\n }) {\n this.modelName = opts.model;\n this.dimensions = opts.dimensions;\n this.batchSize = opts.batchSize || 32;\n this.maxSeqLength = opts.maxSeqLength || DEFAULT_ONNX_MAX_SEQ_LEN;\n this.cacheDir = opts.cacheDir;\n this.modelPath = opts.modelPath;\n this.vocabPath = opts.vocabPath;\n }\n\n private async ensureReady(): Promise<void> {\n if (!this.ready) {\n this.ready = (async () => {\n try {\n this.ort = await loadOnnxRuntime();\n const { modelPath, vocabPath } = await resolveOnnxModelFiles(this.modelName, {\n cacheDir: this.cacheDir,\n modelPath: this.modelPath,\n vocabPath: this.vocabPath,\n });\n const vocab = await loadVocab(vocabPath);\n this.tokenizer = new WordPieceTokenizer(vocab);\n this.session = await this.ort.InferenceSession.create(modelPath);\n } catch (err) {\n this.ready = undefined;\n throw err;\n }\n })();\n }\n await this.ready;\n }\n\n async embed(text: string): Promise<number[]> {\n const results = await this.embedBatch([text]);\n if (results.length === 0) {\n throw new Error(\"ONNX embed returned empty results for single text\");\n }\n return results[0];\n }\n\n async embedBatch(texts: string[]): Promise<number[][]> {\n if (texts.length === 0) return [];\n await this.ensureReady();\n if (!this.session || !this.tokenizer || !this.ort) {\n throw new Error(\"ONNX embedding provider not initialized\");\n }\n\n const allResults: number[][] = [];\n for (let i = 0; i < texts.length; i += this.batchSize) {\n const batch = texts.slice(i, i + this.batchSize);\n const tokenizer = this.tokenizer;\n const encoded = batch.map((t) => tokenizer.encode(t, this.maxSeqLength));\n const maxLen = Math.min(this.maxSeqLength, Math.max(...encoded.map((e) => e.inputIds.length)));\n const padId = tokenizer.getPadTokenId();\n for (const e of encoded) {\n while (e.inputIds.length < maxLen) {\n e.inputIds.push(padId);\n e.attentionMask.push(0);\n e.tokenTypeIds.push(0);\n }\n }\n const batchSize = encoded.length;\n const inputIds = new BigInt64Array(batchSize * maxLen);\n const attentionMask = new BigInt64Array(batchSize * maxLen);\n const tokenTypeIds = new BigInt64Array(batchSize * maxLen);\n for (let b = 0; b < batchSize; b++) {\n const e = encoded[b];\n if (!e) continue;\n for (let t = 0; t < maxLen; t++) {\n const idx = b * maxLen + t;\n inputIds[idx] = BigInt(e.inputIds[t] ?? 0);\n attentionMask[idx] = BigInt(e.attentionMask[t] ?? 0);\n tokenTypeIds[idx] = BigInt(e.tokenTypeIds[t] ?? 0);\n }\n }\n const feeds: Record<string, OnnxTensor> = {};\n const inputNames = this.session.inputNames;\n if (!inputNames.includes(\"input_ids\")) {\n throw new Error(\"ONNX model input does not include input_ids\");\n }\n feeds.input_ids = new this.ort.Tensor(\"int64\", inputIds, [batchSize, maxLen]);\n if (inputNames.includes(\"attention_mask\")) {\n feeds.attention_mask = new this.ort.Tensor(\"int64\", attentionMask, [batchSize, maxLen]);\n }\n if (inputNames.includes(\"token_type_ids\")) {\n feeds.token_type_ids = new this.ort.Tensor(\"int64\", tokenTypeIds, [batchSize, maxLen]);\n }\n\n const output = await this.session.run(feeds);\n const outputTensor = output.sentence_embedding ?? output.pooler_output ?? output.last_hidden_state;\n if (!outputTensor) {\n throw new Error(\"ONNX output missing sentence_embedding/pooler_output/last_hidden_state\");\n }\n const data = outputTensor.data as Float32Array;\n const dims = outputTensor.dims as number[];\n let vectors: Float32Array[] = [];\n if (outputTensor === output.last_hidden_state) {\n const [b, s, h] = dims;\n if (!b || !s || !h) throw new Error(\"ONNX last_hidden_state has invalid dims\");\n vectors = meanPool(data, attentionMask, b, s, h);\n } else {\n const [b, h] = dims;\n if (!b || !h) throw new Error(\"ONNX output has invalid dims\");\n for (let bi = 0; bi < b; bi++) {\n const start = bi * h;\n vectors.push(data.slice(start, start + h));\n }\n }\n if (vectors.length !== batchSize) {\n throw new Error(`ONNX embed returned ${vectors.length} embeddings for ${batchSize} inputs`);\n }\n for (const vec of vectors) {\n const normalized = l2Normalize(vec);\n if (normalized.length !== this.dimensions) {\n throw new Error(`ONNX embedding dimension mismatch: expected ${this.dimensions}, got ${normalized.length}`);\n }\n allResults.push(Array.from(normalized));\n }\n }\n return allResults;\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"onnx-provider.js","names":["fs"],"sources":["../../../services/embeddings/onnx-provider.ts"],"sourcesContent":["/**\n * ONNX local embedding provider.\n */\n\nimport { promises as fs } from \"node:fs\";\nimport { createWriteStream } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { dirname, join, resolve } from \"node:path\";\nimport { Readable, Transform } from \"node:stream\";\nimport { pipeline } from \"node:stream/promises\";\nimport type { EmbeddingProvider } from \"./types.js\";\n\n// ---------------------------------------------------------------------------\n// ONNX type shims (loaded dynamically at runtime; package is optional).\n// ---------------------------------------------------------------------------\n\ninterface OnnxTensor {\n data: Float32Array | Int32Array | BigInt64Array;\n dims: readonly number[];\n}\ninterface OnnxInferenceSession {\n readonly inputNames: readonly string[];\n run(feeds: Record<string, OnnxTensor>): Promise<Record<string, OnnxTensor>>;\n}\ninterface OnnxRuntime {\n InferenceSession: { create(modelPath: string): Promise<OnnxInferenceSession> };\n Tensor: new (type: string, data: BigInt64Array, dims: number[]) => OnnxTensor;\n}\ntype OnnxRuntimeLoader = () => Promise<OnnxRuntime>;\n\nconst DEFAULT_ONNX_CACHE_DIR = join(homedir(), \".cache\", \"openclaw\", \"onnx-embeddings\");\nconst DEFAULT_ONNX_MAX_SEQ_LEN = 256;\nexport const ONNX_RUNTIME_LOAD_TIMEOUT_MS = 30_000;\n\nconst ONNX_MODEL_SPECS: Record<string, { repo: string; modelFile: string; vocabFileCandidates: string[] }> = {\n \"all-MiniLM-L6-v2\": {\n repo: \"sentence-transformers/all-MiniLM-L6-v2\",\n modelFile: \"onnx/model.onnx\",\n vocabFileCandidates: [\"vocab.txt\", \"tokenizer/vocab.txt\"],\n },\n \"bge-small-en-v1.5\": {\n repo: \"BAAI/bge-small-en-v1.5\",\n modelFile: \"onnx/model.onnx\",\n vocabFileCandidates: [\"vocab.txt\", \"tokenizer/vocab.txt\"],\n },\n};\n\nclass OnnxRuntimeMissingError extends Error {\n readonly code = \"ONNX_RUNTIME_MISSING\";\n constructor(message: string) {\n super(message);\n this.name = \"OnnxRuntimeMissingError\";\n }\n}\n\nclass OnnxRuntimeLoadTimeoutError extends Error {\n readonly code = \"ONNX_RUNTIME_LOAD_TIMEOUT\";\n constructor(timeoutMs: number) {\n super(`Timed out loading onnxruntime-node after ${timeoutMs}ms.`);\n this.name = \"OnnxRuntimeLoadTimeoutError\";\n }\n}\n\nexport function isOnnxRuntimeMissingError(err: unknown): err is OnnxRuntimeMissingError {\n return (\n err instanceof OnnxRuntimeMissingError ||\n (err instanceof Error && (err as Error & { code?: string }).code === \"ONNX_RUNTIME_MISSING\")\n );\n}\n\nconst defaultOnnxRuntimeLoader: OnnxRuntimeLoader = () =>\n import(\"onnxruntime-node\") as Promise<unknown> as Promise<OnnxRuntime>;\nlet onnxRuntimeLoader: OnnxRuntimeLoader = defaultOnnxRuntimeLoader;\n\n/** @internal Test hook: override ONNX runtime loader. */\nexport function __setOnnxRuntimeLoaderForTests(loader: OnnxRuntimeLoader | null): void {\n onnxRuntimeLoader = loader ?? defaultOnnxRuntimeLoader;\n}\n\nasync function loadOnnxRuntime(): Promise<OnnxRuntime> {\n let timeoutId: ReturnType<typeof setTimeout> | undefined;\n try {\n const runtime = await Promise.race([\n onnxRuntimeLoader(),\n new Promise<never>((_, reject) => {\n timeoutId = setTimeout(\n () => reject(new OnnxRuntimeLoadTimeoutError(ONNX_RUNTIME_LOAD_TIMEOUT_MS)),\n ONNX_RUNTIME_LOAD_TIMEOUT_MS,\n );\n }),\n ]);\n if (timeoutId) {\n clearTimeout(timeoutId);\n timeoutId = undefined;\n }\n return runtime;\n } catch (err) {\n if (err instanceof OnnxRuntimeLoadTimeoutError) {\n throw err;\n }\n throw new OnnxRuntimeMissingError(\n \"onnxruntime-node is not installed. Install it to use provider='onnx' (e.g. npm i onnxruntime-node).\",\n );\n } finally {\n if (timeoutId) {\n clearTimeout(timeoutId);\n }\n }\n}\n\nasync function fileExists(path: string): Promise<boolean> {\n try {\n await fs.access(path);\n return true;\n } catch {\n return false;\n }\n}\n\nfunction sanitizeRepoDir(repo: string): string {\n return repo.replace(/[^\\w.-]+/g, \"__\");\n}\n\nfunction splitRepoAndRevision(input: string): { repo: string; revision: string } {\n const at = input.lastIndexOf(\"@\");\n if (at > 0 && at < input.length - 1) {\n return { repo: input.slice(0, at), revision: input.slice(at + 1) };\n }\n return { repo: input, revision: \"main\" };\n}\n\nconst ONNX_DOWNLOAD_TIMEOUT_MS = 120_000;\nconst ONNX_DOWNLOAD_MAX_BYTES = 500 * 1024 * 1024; // 500 MiB\n\nasync function downloadFile(url: string, destPath: string): Promise<void> {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), ONNX_DOWNLOAD_TIMEOUT_MS);\n const resp = await fetch(url, { signal: controller.signal });\n clearTimeout(timeoutId);\n if (!resp.ok || !resp.body) {\n const body = await resp.text().catch(() => \"\");\n throw new Error(\n `Failed to download ${url}: HTTP ${resp.status} ${resp.statusText}${body ? ` — ${body.slice(0, 200)}` : \"\"}`,\n );\n }\n const contentLength = resp.headers.get(\"content-length\");\n if (contentLength) {\n const len = Number.parseInt(contentLength, 10);\n if (!Number.isNaN(len) && len > ONNX_DOWNLOAD_MAX_BYTES) {\n throw new Error(`Refusing to download ${url}: size ${len} exceeds limit ${ONNX_DOWNLOAD_MAX_BYTES}`);\n }\n }\n await fs.mkdir(dirname(destPath), { recursive: true });\n const tempPath = `${destPath}.tmp`;\n const stream = Readable.fromWeb(resp.body as import(\"stream/web\").ReadableStream<Uint8Array>);\n try {\n let bytesWritten = 0;\n const countStream = new Transform({\n transform(chunk: Buffer, _enc, cb) {\n bytesWritten += chunk.length;\n if (bytesWritten > ONNX_DOWNLOAD_MAX_BYTES) {\n cb(new Error(`Download size exceeds limit ${ONNX_DOWNLOAD_MAX_BYTES}`));\n return;\n }\n cb(null, chunk);\n },\n });\n await pipeline(stream, countStream, createWriteStream(tempPath));\n await fs.rename(tempPath, destPath);\n } catch (err) {\n await fs.unlink(tempPath).catch(() => {});\n throw err;\n }\n}\n\nasync function resolveOnnxModelFiles(\n model: string,\n opts?: { cacheDir?: string; modelPath?: string; vocabPath?: string },\n): Promise<{ modelPath: string; vocabPath: string }> {\n const cacheDir = opts?.cacheDir ?? DEFAULT_ONNX_CACHE_DIR;\n if (opts?.modelPath) {\n const modelPath = resolve(opts.modelPath);\n if (!(await fileExists(modelPath))) {\n throw new Error(`ONNX model file not found at ${modelPath}`);\n }\n const vocabPath = opts?.vocabPath ? resolve(opts.vocabPath) : join(dirname(modelPath), \"vocab.txt\");\n if (!(await fileExists(vocabPath))) {\n throw new Error(`Tokenizer vocab.txt not found at ${vocabPath}`);\n }\n return { modelPath, vocabPath };\n }\n\n const resolvedModelPath = resolve(model);\n if (model.endsWith(\".onnx\") && (await fileExists(resolvedModelPath))) {\n const vocabPath = opts?.vocabPath ? resolve(opts.vocabPath) : join(dirname(resolvedModelPath), \"vocab.txt\");\n if (!(await fileExists(vocabPath))) {\n throw new Error(`Tokenizer vocab.txt not found at ${vocabPath}`);\n }\n return { modelPath: resolvedModelPath, vocabPath };\n }\n\n const spec = ONNX_MODEL_SPECS[model];\n const { repo, revision } = splitRepoAndRevision(spec?.repo ?? model);\n const modelFile = spec?.modelFile ?? \"onnx/model.onnx\";\n const vocabCandidates = spec?.vocabFileCandidates ?? [\"vocab.txt\", \"tokenizer/vocab.txt\"];\n const repoDir = join(cacheDir, sanitizeRepoDir(repo), revision);\n const modelPath = join(repoDir, modelFile);\n if (!(await fileExists(modelPath))) {\n const modelUrl = `https://huggingface.co/${repo}/resolve/${revision}/${modelFile}`;\n await downloadFile(modelUrl, modelPath);\n }\n let vocabPath = opts?.vocabPath ? resolve(opts.vocabPath) : \"\";\n if (vocabPath && !(await fileExists(vocabPath))) {\n throw new Error(`Tokenizer vocab.txt not found at ${vocabPath}`);\n }\n if (!vocabPath) {\n for (const candidate of vocabCandidates) {\n const candidatePath = join(repoDir, candidate);\n if (await fileExists(candidatePath)) {\n vocabPath = candidatePath;\n break;\n }\n const vocabUrl = `https://huggingface.co/${repo}/resolve/${revision}/${candidate}`;\n try {\n await downloadFile(vocabUrl, candidatePath);\n vocabPath = candidatePath;\n break;\n } catch {}\n }\n }\n if (!vocabPath) {\n throw new Error(`Unable to locate or download vocab.txt for model '${model}'.`);\n }\n return { modelPath, vocabPath };\n}\n\n// ---------------------------------------------------------------------------\n// Tokenizer helpers\n// ---------------------------------------------------------------------------\n\nfunction normalizeText(text: string): string {\n return text.normalize(\"NFD\").replace(/[\\u0300-\\u036f]/g, \"\");\n}\n\nfunction isPunctuation(char: string): boolean {\n const code = char.charCodeAt(0);\n return (\n (code >= 33 && code <= 47) ||\n (code >= 58 && code <= 64) ||\n (code >= 91 && code <= 96) ||\n (code >= 123 && code <= 126)\n );\n}\n\nfunction basicTokenize(text: string): string[] {\n const normalized = normalizeText(text).toLowerCase();\n const tokens: string[] = [];\n let current = \"\";\n for (const ch of normalized) {\n if (/\\s/.test(ch)) {\n if (current) {\n tokens.push(current);\n current = \"\";\n }\n continue;\n }\n if (isPunctuation(ch)) {\n if (current) {\n tokens.push(current);\n current = \"\";\n }\n tokens.push(ch);\n continue;\n }\n current += ch;\n }\n if (current) tokens.push(current);\n return tokens;\n}\n\nfunction wordPieceTokenize(token: string, vocab: Map<string, number>, unkToken: string, maxChars = 100): string[] {\n if (token.length > maxChars) return [unkToken];\n if (vocab.has(token)) return [token];\n const chars = token.split(\"\");\n const pieces: string[] = [];\n let start = 0;\n while (start < chars.length) {\n let end = chars.length;\n let found = \"\";\n while (start < end) {\n const substr = chars.slice(start, end).join(\"\");\n const candidate = start === 0 ? substr : `##${substr}`;\n if (vocab.has(candidate)) {\n found = candidate;\n break;\n }\n end -= 1;\n }\n if (!found) {\n return [unkToken];\n }\n pieces.push(found);\n start = end;\n }\n return pieces;\n}\n\ntype TokenizedInput = { inputIds: number[]; attentionMask: number[]; tokenTypeIds: number[] };\n\nclass WordPieceTokenizer {\n private vocab: Map<string, number>;\n private readonly clsToken: string;\n private readonly sepToken: string;\n private readonly padToken: string;\n private readonly unkToken: string;\n\n constructor(vocab: Map<string, number>) {\n this.vocab = vocab;\n this.clsToken = \"[CLS]\";\n this.sepToken = \"[SEP]\";\n this.padToken = \"[PAD]\";\n this.unkToken = \"[UNK]\";\n for (const tok of [this.clsToken, this.sepToken, this.padToken, this.unkToken]) {\n if (!this.vocab.has(tok)) {\n throw new Error(`Tokenizer vocab missing required token ${tok}`);\n }\n }\n }\n\n encode(text: string, maxLen: number): TokenizedInput {\n const tokens = basicTokenize(text).flatMap((t) => wordPieceTokenize(t, this.vocab, this.unkToken));\n const maxTokens = Math.max(0, maxLen - 2);\n const trimmed = tokens.length > maxTokens ? tokens.slice(0, maxTokens) : tokens;\n const finalTokens = [this.clsToken, ...trimmed, this.sepToken];\n const inputIds = finalTokens.map((t) => this.vocab.get(t) ?? this.vocab.get(this.unkToken)!);\n const attentionMask = new Array(finalTokens.length).fill(1);\n const tokenTypeIds = new Array(finalTokens.length).fill(0);\n return { inputIds, attentionMask, tokenTypeIds };\n }\n\n getPadTokenId(): number {\n return this.vocab.get(this.padToken)!;\n }\n}\n\nasync function loadVocab(vocabPath: string): Promise<Map<string, number>> {\n const content = await fs.readFile(vocabPath, \"utf-8\");\n const vocab = new Map<string, number>();\n const lines = content.split(/\\r?\\n/);\n for (let i = 0; i < lines.length; i++) {\n const token = lines[i].trim();\n if (!token) continue;\n vocab.set(token, i);\n }\n return vocab;\n}\n\nfunction l2Normalize(vec: Float32Array): Float32Array {\n let sum = 0;\n for (const v of vec) sum += v * v;\n const norm = Math.sqrt(sum);\n if (!norm) return vec;\n const out = new Float32Array(vec.length);\n for (let i = 0; i < vec.length; i++) out[i] = vec[i] / norm;\n return out;\n}\n\nfunction meanPool(\n lastHidden: Float32Array,\n attentionMask: BigInt64Array,\n batch: number,\n seq: number,\n hidden: number,\n): Float32Array[] {\n const outputs: Float32Array[] = [];\n let offset = 0;\n for (let b = 0; b < batch; b++) {\n const out = new Float32Array(hidden);\n let count = 0;\n for (let s = 0; s < seq; s++) {\n const mask = attentionMask[b * seq + s];\n if (mask === 0n) {\n offset += hidden;\n continue;\n }\n for (let h = 0; h < hidden; h++) {\n out[h] += lastHidden[offset + h];\n }\n count++;\n offset += hidden;\n }\n if (count > 0) {\n for (let h = 0; h < hidden; h++) out[h] /= count;\n }\n outputs.push(out);\n }\n return outputs;\n}\n\n// ---------------------------------------------------------------------------\n// OnnxEmbeddingProvider\n// ---------------------------------------------------------------------------\n\nexport class OnnxEmbeddingProvider implements EmbeddingProvider {\n readonly dimensions: number;\n readonly modelName: string;\n private readonly batchSize: number;\n private readonly maxSeqLength: number;\n private readonly cacheDir?: string;\n private readonly modelPath?: string;\n private readonly vocabPath?: string;\n private session?: OnnxInferenceSession;\n private tokenizer?: WordPieceTokenizer;\n private ort?: OnnxRuntime;\n private ready?: Promise<void>;\n\n constructor(opts: {\n model: string;\n dimensions: number;\n batchSize?: number;\n maxSeqLength?: number;\n cacheDir?: string;\n modelPath?: string;\n vocabPath?: string;\n }) {\n this.modelName = opts.model;\n this.dimensions = opts.dimensions;\n this.batchSize = opts.batchSize || 32;\n this.maxSeqLength = opts.maxSeqLength || DEFAULT_ONNX_MAX_SEQ_LEN;\n this.cacheDir = opts.cacheDir;\n this.modelPath = opts.modelPath;\n this.vocabPath = opts.vocabPath;\n }\n\n private async ensureReady(): Promise<void> {\n if (!this.ready) {\n this.ready = (async () => {\n try {\n this.ort = await loadOnnxRuntime();\n const { modelPath, vocabPath } = await resolveOnnxModelFiles(this.modelName, {\n cacheDir: this.cacheDir,\n modelPath: this.modelPath,\n vocabPath: this.vocabPath,\n });\n const vocab = await loadVocab(vocabPath);\n this.tokenizer = new WordPieceTokenizer(vocab);\n this.session = await this.ort.InferenceSession.create(modelPath);\n } catch (err) {\n this.ready = undefined;\n throw err;\n }\n })();\n }\n await this.ready;\n }\n\n async embed(text: string): Promise<number[]> {\n const results = await this.embedBatch([text]);\n if (results.length === 0) {\n throw new Error(\"ONNX embed returned empty results for single text\");\n }\n return results[0];\n }\n\n async embedBatch(texts: string[]): Promise<number[][]> {\n if (texts.length === 0) return [];\n await this.ensureReady();\n if (!this.session || !this.tokenizer || !this.ort) {\n throw new Error(\"ONNX embedding provider not initialized\");\n }\n\n const allResults: number[][] = [];\n for (let i = 0; i < texts.length; i += this.batchSize) {\n const batch = texts.slice(i, i + this.batchSize);\n const tokenizer = this.tokenizer;\n const encoded = batch.map((t) => tokenizer.encode(t, this.maxSeqLength));\n const maxLen = Math.min(this.maxSeqLength, Math.max(...encoded.map((e) => e.inputIds.length)));\n const padId = tokenizer.getPadTokenId();\n for (const e of encoded) {\n while (e.inputIds.length < maxLen) {\n e.inputIds.push(padId);\n e.attentionMask.push(0);\n e.tokenTypeIds.push(0);\n }\n }\n const batchSize = encoded.length;\n const inputIds = new BigInt64Array(batchSize * maxLen);\n const attentionMask = new BigInt64Array(batchSize * maxLen);\n const tokenTypeIds = new BigInt64Array(batchSize * maxLen);\n for (let b = 0; b < batchSize; b++) {\n const e = encoded[b];\n if (!e) continue;\n for (let t = 0; t < maxLen; t++) {\n const idx = b * maxLen + t;\n inputIds[idx] = BigInt(e.inputIds[t] ?? 0);\n attentionMask[idx] = BigInt(e.attentionMask[t] ?? 0);\n tokenTypeIds[idx] = BigInt(e.tokenTypeIds[t] ?? 0);\n }\n }\n const feeds: Record<string, OnnxTensor> = {};\n const inputNames = this.session.inputNames;\n if (!inputNames.includes(\"input_ids\")) {\n throw new Error(\"ONNX model input does not include input_ids\");\n }\n feeds.input_ids = new this.ort.Tensor(\"int64\", inputIds, [batchSize, maxLen]);\n if (inputNames.includes(\"attention_mask\")) {\n feeds.attention_mask = new this.ort.Tensor(\"int64\", attentionMask, [batchSize, maxLen]);\n }\n if (inputNames.includes(\"token_type_ids\")) {\n feeds.token_type_ids = new this.ort.Tensor(\"int64\", tokenTypeIds, [batchSize, maxLen]);\n }\n\n const output = await this.session.run(feeds);\n const outputTensor = output.sentence_embedding ?? output.pooler_output ?? output.last_hidden_state;\n if (!outputTensor) {\n throw new Error(\"ONNX output missing sentence_embedding/pooler_output/last_hidden_state\");\n }\n const data = outputTensor.data as Float32Array;\n const dims = outputTensor.dims as number[];\n let vectors: Float32Array[] = [];\n if (outputTensor === output.last_hidden_state) {\n const [b, s, h] = dims;\n if (!b || !s || !h) throw new Error(\"ONNX last_hidden_state has invalid dims\");\n vectors = meanPool(data, attentionMask, b, s, h);\n } else {\n const [b, h] = dims;\n if (!b || !h) throw new Error(\"ONNX output has invalid dims\");\n for (let bi = 0; bi < b; bi++) {\n const start = bi * h;\n vectors.push(data.slice(start, start + h));\n }\n }\n if (vectors.length !== batchSize) {\n throw new Error(`ONNX embed returned ${vectors.length} embeddings for ${batchSize} inputs`);\n }\n for (const vec of vectors) {\n const normalized = l2Normalize(vec);\n if (normalized.length !== this.dimensions) {\n throw new Error(`ONNX embedding dimension mismatch: expected ${this.dimensions}, got ${normalized.length}`);\n }\n allResults.push(Array.from(normalized));\n }\n }\n return allResults;\n }\n}\n"],"mappings":"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"}
|