mdcontext 0.0.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.changeset/README.md +28 -0
- package/.changeset/config.json +11 -0
- package/.claude/settings.local.json +25 -0
- package/.github/workflows/ci.yml +83 -0
- package/.github/workflows/claude-code-review.yml +44 -0
- package/.github/workflows/claude.yml +85 -0
- package/.github/workflows/release.yml +113 -0
- package/.tldrignore +112 -0
- package/BACKLOG.md +338 -0
- package/CONTRIBUTING.md +186 -0
- package/NOTES/NOTES +44 -0
- package/README.md +434 -11
- package/biome.json +36 -0
- package/cspell.config.yaml +14 -0
- package/dist/chunk-23UPXDNL.js +3044 -0
- package/dist/chunk-2W7MO2DL.js +1366 -0
- package/dist/chunk-3NUAZGMA.js +1689 -0
- package/dist/chunk-7TOWB2XB.js +366 -0
- package/dist/chunk-7XOTOADQ.js +3065 -0
- package/dist/chunk-AH2PDM2K.js +3042 -0
- package/dist/chunk-BNXWSZ63.js +3742 -0
- package/dist/chunk-BTL5DJVU.js +3222 -0
- package/dist/chunk-HDHYG7E4.js +104 -0
- package/dist/chunk-HLR4KZBP.js +3234 -0
- package/dist/chunk-IP3FRFEB.js +1045 -0
- package/dist/chunk-KHU56VDO.js +3042 -0
- package/dist/chunk-KRYIFLQR.js +88 -0
- package/dist/chunk-LBSDNLEM.js +287 -0
- package/dist/chunk-MNTQ7HCP.js +2643 -0
- package/dist/chunk-MUJELQQ6.js +1387 -0
- package/dist/chunk-MXJGMSLV.js +2199 -0
- package/dist/chunk-N6QJGC3Z.js +2636 -0
- package/dist/chunk-OBELGBPM.js +1713 -0
- package/dist/chunk-OT7R5XTA.js +3192 -0
- package/dist/chunk-P7X4RA2T.js +106 -0
- package/dist/chunk-PIDUQNC2.js +3185 -0
- package/dist/chunk-POGCDIH4.js +3187 -0
- package/dist/chunk-PSIEOQGZ.js +3043 -0
- package/dist/chunk-PVRT3IHA.js +3238 -0
- package/dist/chunk-QNN4TT23.js +1430 -0
- package/dist/chunk-RE3R45RJ.js +3042 -0
- package/dist/chunk-S7E6TFX6.js +803 -0
- package/dist/chunk-SG6GLU4U.js +1378 -0
- package/dist/chunk-SJCDV2ST.js +274 -0
- package/dist/chunk-SYE5XLF3.js +104 -0
- package/dist/chunk-T5VLYBZD.js +103 -0
- package/dist/chunk-TOQB7VWU.js +3238 -0
- package/dist/chunk-VFNMZ4ZQ.js +3228 -0
- package/dist/chunk-VVTGZNBT.js +1629 -0
- package/dist/chunk-W7Q4RFEV.js +104 -0
- package/dist/chunk-XTYYVRLO.js +3190 -0
- package/dist/chunk-Y6MDYVJD.js +3063 -0
- package/dist/cli/main.d.ts +1 -0
- package/dist/cli/main.js +5458 -0
- package/dist/index.d.ts +653 -0
- package/dist/index.js +79 -0
- package/dist/mcp/server.d.ts +1 -0
- package/dist/mcp/server.js +472 -0
- package/dist/schema-BAWSG7KY.js +22 -0
- package/dist/schema-E3QUPL26.js +20 -0
- package/dist/schema-EHL7WUT6.js +20 -0
- package/docs/019-USAGE.md +625 -0
- package/docs/020-current-implementation.md +364 -0
- package/docs/021-DOGFOODING-FINDINGS.md +175 -0
- package/docs/BACKLOG.md +80 -0
- package/docs/CONFIG.md +1123 -0
- package/docs/DESIGN.md +439 -0
- package/docs/ERRORS.md +383 -0
- package/docs/PROJECT.md +88 -0
- package/docs/ROADMAP.md +407 -0
- package/docs/summarization.md +320 -0
- package/docs/test-links.md +9 -0
- package/justfile +40 -0
- package/package.json +74 -9
- package/pnpm-workspace.yaml +5 -0
- package/research/INDEX.md +315 -0
- package/research/code-review/README.md +90 -0
- package/research/code-review/cli-error-handling-review.md +979 -0
- package/research/code-review/code-review-validation-report.md +464 -0
- package/research/code-review/main-ts-review.md +1128 -0
- package/research/config-analysis/01-current-implementation.md +470 -0
- package/research/config-analysis/02-strategy-recommendation.md +428 -0
- package/research/config-analysis/03-task-candidates.md +715 -0
- package/research/config-analysis/033-research-configuration-management.md +828 -0
- package/research/config-analysis/034-research-effect-cli-config.md +1504 -0
- package/research/config-analysis/04-consolidated-task-candidates.md +277 -0
- package/research/config-docs/SUMMARY.md +357 -0
- package/research/config-docs/TEST-RESULTS.md +776 -0
- package/research/config-docs/TODO.md +542 -0
- package/research/config-docs/analysis.md +744 -0
- package/research/config-docs/fix-validation.md +502 -0
- package/research/config-docs/help-audit.md +264 -0
- package/research/config-docs/help-system-analysis.md +890 -0
- package/research/dogfood/consolidated-tool-evaluation.md +373 -0
- package/research/dogfood/strategy-a/a-synthesis.md +184 -0
- package/research/dogfood/strategy-a/a1-docs.md +226 -0
- package/research/dogfood/strategy-a/a2-amorphic.md +156 -0
- package/research/dogfood/strategy-a/a3-llm.md +164 -0
- package/research/dogfood/strategy-b/b-synthesis.md +228 -0
- package/research/dogfood/strategy-b/b1-architecture.md +207 -0
- package/research/dogfood/strategy-b/b2-gaps.md +258 -0
- package/research/dogfood/strategy-b/b3-workflows.md +250 -0
- package/research/dogfood/strategy-c/c-synthesis.md +451 -0
- package/research/dogfood/strategy-c/c1-explorer.md +192 -0
- package/research/dogfood/strategy-c/c2-diver-memory.md +145 -0
- package/research/dogfood/strategy-c/c3-diver-control.md +148 -0
- package/research/dogfood/strategy-c/c4-diver-failure.md +151 -0
- package/research/dogfood/strategy-c/c5-diver-execution.md +221 -0
- package/research/dogfood/strategy-c/c6-diver-org.md +221 -0
- package/research/effect-cli-error-handling.md +845 -0
- package/research/effect-errors-as-values.md +943 -0
- package/research/errors-task-analysis/00-consolidated-tasks.md +207 -0
- package/research/errors-task-analysis/cli-commands-analysis.md +909 -0
- package/research/errors-task-analysis/embeddings-analysis.md +709 -0
- package/research/errors-task-analysis/index-search-analysis.md +812 -0
- package/research/frontmatter/COMMENTS-ARE-SKIPPED.md +149 -0
- package/research/frontmatter/LLM-CODE-NAVIGATION.md +276 -0
- package/research/issue-review.md +603 -0
- package/research/llm-summarization/agent-cli-tools-2026.md +1082 -0
- package/research/llm-summarization/alternative-providers-2026.md +1428 -0
- package/research/llm-summarization/anthropic-2026.md +367 -0
- package/research/llm-summarization/claude-cli-integration.md +1706 -0
- package/research/llm-summarization/cli-integration-patterns.md +3155 -0
- package/research/llm-summarization/openai-2026.md +473 -0
- package/research/llm-summarization/openai-compatible-providers-2026.md +1022 -0
- package/research/llm-summarization/opencode-cli-integration.md +1552 -0
- package/research/llm-summarization/prompt-engineering-2026.md +1426 -0
- package/research/llm-summarization/prototype-results.md +56 -0
- package/research/llm-summarization/provider-switching-patterns-2026.md +2153 -0
- package/research/llm-summarization/typescript-llm-libraries-2026.md +2436 -0
- package/research/mdcontext-error-analysis.md +521 -0
- package/research/mdcontext-pudding/00-EXECUTIVE-SUMMARY.md +282 -0
- package/research/mdcontext-pudding/01-index-embed.md +956 -0
- package/research/mdcontext-pudding/02-search-COMMANDS.md +142 -0
- package/research/mdcontext-pudding/02-search-SUMMARY.md +146 -0
- package/research/mdcontext-pudding/02-search.md +970 -0
- package/research/mdcontext-pudding/03-context.md +779 -0
- package/research/mdcontext-pudding/04-navigation-and-analytics.md +803 -0
- package/research/mdcontext-pudding/04-tree.md +704 -0
- package/research/mdcontext-pudding/05-config.md +1038 -0
- package/research/mdcontext-pudding/06-links-summary.txt +87 -0
- package/research/mdcontext-pudding/06-links.md +679 -0
- package/research/mdcontext-pudding/07-stats.md +693 -0
- package/research/mdcontext-pudding/BUG-FIX-PLAN.md +388 -0
- package/research/mdcontext-pudding/P0-BUG-VALIDATION.md +167 -0
- package/research/mdcontext-pudding/README.md +168 -0
- package/research/mdcontext-pudding/TESTING-SUMMARY.md +128 -0
- package/research/npm_publish/011-npm-workflow-research-agent2.md +792 -0
- package/research/npm_publish/012-npm-workflow-research-agent1.md +530 -0
- package/research/npm_publish/013-npm-workflow-research-agent3.md +722 -0
- package/research/npm_publish/014-npm-workflow-synthesis.md +556 -0
- package/research/npm_publish/031-npm-workflow-task-analysis.md +134 -0
- package/research/research-quality-review.md +834 -0
- package/research/semantic-search/002-research-embedding-models.md +490 -0
- package/research/semantic-search/003-research-rag-alternatives.md +523 -0
- package/research/semantic-search/004-research-vector-search.md +841 -0
- package/research/semantic-search/032-research-semantic-search.md +427 -0
- package/research/semantic-search/embedding-text-analysis.md +156 -0
- package/research/semantic-search/multi-word-failure-reproduction.md +171 -0
- package/research/semantic-search/query-processing-analysis.md +207 -0
- package/research/semantic-search/root-cause-and-solution.md +114 -0
- package/research/semantic-search/threshold-validation-report.md +69 -0
- package/research/semantic-search/vector-search-analysis.md +63 -0
- package/research/task-management-2026/00-synthesis-recommendations.md +295 -0
- package/research/task-management-2026/01-ai-workflow-tools.md +416 -0
- package/research/task-management-2026/02-agent-framework-patterns.md +476 -0
- package/research/task-management-2026/03-lightweight-file-based.md +567 -0
- package/research/task-management-2026/04-established-tools-ai-features.md +541 -0
- package/research/task-management-2026/linear/01-core-features-workflow.md +771 -0
- package/research/task-management-2026/linear/02-api-integrations.md +930 -0
- package/research/task-management-2026/linear/03-ai-features.md +368 -0
- package/research/task-management-2026/linear/04-pricing-setup.md +205 -0
- package/research/task-management-2026/linear/05-usage-patterns-best-practices.md +605 -0
- package/research/test-path-issues.md +276 -0
- package/review/ALP-76/1-error-type-design.md +962 -0
- package/review/ALP-76/2-error-handling-patterns.md +906 -0
- package/review/ALP-76/3-error-presentation.md +624 -0
- package/review/ALP-76/4-test-coverage.md +625 -0
- package/review/ALP-76/5-migration-completeness.md +440 -0
- package/review/ALP-76/6-effect-best-practices.md +755 -0
- package/scripts/apply-branch-protection.sh +47 -0
- package/scripts/branch-protection-templates.json +79 -0
- package/scripts/prototype-summarization.ts +346 -0
- package/scripts/rebuild-hnswlib.js +58 -0
- package/scripts/setup-branch-protection.sh +64 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/active-provider.json +7 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/bm25.json +541 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/bm25.meta.json +5 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/config.json +8 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/embeddings/openai_text-embedding-3-small_512/vectors.bin +0 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/embeddings/openai_text-embedding-3-small_512/vectors.meta.bin +0 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/indexes/documents.json +60 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/indexes/links.json +13 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/indexes/sections.json +1197 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/configuration-management.md +99 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/distributed-systems.md +92 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/error-handling.md +78 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/failure-automation.md +55 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/job-context.md +69 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/process-orchestration.md +99 -0
- package/src/cli/argv-preprocessor.test.ts +210 -0
- package/src/cli/argv-preprocessor.ts +202 -0
- package/src/cli/cli.test.ts +627 -0
- package/src/cli/commands/backlinks.ts +54 -0
- package/src/cli/commands/config-cmd.ts +642 -0
- package/src/cli/commands/context.ts +285 -0
- package/src/cli/commands/duplicates.ts +122 -0
- package/src/cli/commands/embeddings.ts +529 -0
- package/src/cli/commands/index-cmd.ts +480 -0
- package/src/cli/commands/index.ts +16 -0
- package/src/cli/commands/links.ts +52 -0
- package/src/cli/commands/search.ts +1281 -0
- package/src/cli/commands/stats.ts +149 -0
- package/src/cli/commands/tree.ts +128 -0
- package/src/cli/config-layer.ts +176 -0
- package/src/cli/error-handler.test.ts +235 -0
- package/src/cli/error-handler.ts +655 -0
- package/src/cli/flag-schemas.ts +341 -0
- package/src/cli/help.ts +588 -0
- package/src/cli/index.ts +9 -0
- package/src/cli/main.ts +435 -0
- package/src/cli/options.ts +41 -0
- package/src/cli/shared-error-handling.ts +199 -0
- package/src/cli/typo-suggester.test.ts +105 -0
- package/src/cli/typo-suggester.ts +130 -0
- package/src/cli/utils.ts +259 -0
- package/src/config/file-provider.test.ts +320 -0
- package/src/config/file-provider.ts +273 -0
- package/src/config/index.ts +72 -0
- package/src/config/integration.test.ts +667 -0
- package/src/config/precedence.test.ts +277 -0
- package/src/config/precedence.ts +451 -0
- package/src/config/schema.test.ts +414 -0
- package/src/config/schema.ts +603 -0
- package/src/config/service.test.ts +320 -0
- package/src/config/service.ts +243 -0
- package/src/config/testing.test.ts +264 -0
- package/src/config/testing.ts +110 -0
- package/src/core/index.ts +1 -0
- package/src/core/types.ts +113 -0
- package/src/duplicates/detector.test.ts +183 -0
- package/src/duplicates/detector.ts +414 -0
- package/src/duplicates/index.ts +18 -0
- package/src/embeddings/embedding-namespace.test.ts +300 -0
- package/src/embeddings/embedding-namespace.ts +947 -0
- package/src/embeddings/heading-boost.test.ts +222 -0
- package/src/embeddings/hnsw-build-options.test.ts +198 -0
- package/src/embeddings/hyde.test.ts +272 -0
- package/src/embeddings/hyde.ts +264 -0
- package/src/embeddings/index.ts +10 -0
- package/src/embeddings/openai-provider.ts +414 -0
- package/src/embeddings/pricing.json +22 -0
- package/src/embeddings/provider-constants.ts +204 -0
- package/src/embeddings/provider-errors.test.ts +967 -0
- package/src/embeddings/provider-errors.ts +565 -0
- package/src/embeddings/provider-factory.test.ts +240 -0
- package/src/embeddings/provider-factory.ts +225 -0
- package/src/embeddings/provider-integration.test.ts +788 -0
- package/src/embeddings/query-preprocessing.test.ts +187 -0
- package/src/embeddings/semantic-search-threshold.test.ts +508 -0
- package/src/embeddings/semantic-search.ts +1270 -0
- package/src/embeddings/types.ts +359 -0
- package/src/embeddings/vector-store.ts +708 -0
- package/src/embeddings/voyage-provider.ts +313 -0
- package/src/errors/errors.test.ts +845 -0
- package/src/errors/index.ts +533 -0
- package/src/index/ignore-patterns.test.ts +354 -0
- package/src/index/ignore-patterns.ts +305 -0
- package/src/index/index.ts +4 -0
- package/src/index/indexer.ts +684 -0
- package/src/index/storage.ts +260 -0
- package/src/index/types.ts +147 -0
- package/src/index/watcher.ts +189 -0
- package/src/index.ts +30 -0
- package/src/integration/search-keyword.test.ts +678 -0
- package/src/mcp/server.ts +612 -0
- package/src/parser/index.ts +1 -0
- package/src/parser/parser.test.ts +291 -0
- package/src/parser/parser.ts +394 -0
- package/src/parser/section-filter.test.ts +277 -0
- package/src/parser/section-filter.ts +392 -0
- package/src/search/__tests__/hybrid-search.test.ts +650 -0
- package/src/search/bm25-store.ts +366 -0
- package/src/search/cross-encoder.test.ts +253 -0
- package/src/search/cross-encoder.ts +406 -0
- package/src/search/fuzzy-search.test.ts +419 -0
- package/src/search/fuzzy-search.ts +273 -0
- package/src/search/hybrid-search.ts +448 -0
- package/src/search/path-matcher.test.ts +276 -0
- package/src/search/path-matcher.ts +33 -0
- package/src/search/query-parser.test.ts +260 -0
- package/src/search/query-parser.ts +319 -0
- package/src/search/searcher.test.ts +280 -0
- package/src/search/searcher.ts +724 -0
- package/src/search/wink-bm25.d.ts +30 -0
- package/src/summarization/cli-providers/claude.ts +202 -0
- package/src/summarization/cli-providers/detection.test.ts +273 -0
- package/src/summarization/cli-providers/detection.ts +118 -0
- package/src/summarization/cli-providers/index.ts +8 -0
- package/src/summarization/cost.test.ts +139 -0
- package/src/summarization/cost.ts +102 -0
- package/src/summarization/error-handler.test.ts +127 -0
- package/src/summarization/error-handler.ts +111 -0
- package/src/summarization/index.ts +102 -0
- package/src/summarization/pipeline.test.ts +498 -0
- package/src/summarization/pipeline.ts +231 -0
- package/src/summarization/prompts.test.ts +269 -0
- package/src/summarization/prompts.ts +133 -0
- package/src/summarization/provider-factory.test.ts +396 -0
- package/src/summarization/provider-factory.ts +178 -0
- package/src/summarization/types.ts +184 -0
- package/src/summarize/budget-bugs.test.ts +620 -0
- package/src/summarize/formatters.ts +419 -0
- package/src/summarize/index.ts +20 -0
- package/src/summarize/summarizer.test.ts +275 -0
- package/src/summarize/summarizer.ts +597 -0
- package/src/summarize/verify-bugs.test.ts +238 -0
- package/src/types/huggingface-transformers.d.ts +66 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/tokens.test.ts +142 -0
- package/src/utils/tokens.ts +186 -0
- package/tests/fixtures/cli/.mdcontext/active-provider.json +7 -0
- package/tests/fixtures/cli/.mdcontext/config.json +8 -0
- package/tests/fixtures/cli/.mdcontext/embeddings/openai_text-embedding-3-small_512/vectors.bin +0 -0
- package/tests/fixtures/cli/.mdcontext/embeddings/openai_text-embedding-3-small_512/vectors.meta.bin +0 -0
- package/tests/fixtures/cli/.mdcontext/indexes/documents.json +33 -0
- package/tests/fixtures/cli/.mdcontext/indexes/links.json +12 -0
- package/tests/fixtures/cli/.mdcontext/indexes/sections.json +247 -0
- package/tests/fixtures/cli/README.md +9 -0
- package/tests/fixtures/cli/api-reference.md +11 -0
- package/tests/fixtures/cli/getting-started.md +11 -0
- package/tests/integration/embed-index.test.ts +712 -0
- package/tests/integration/search-context.test.ts +469 -0
- package/tests/integration/search-semantic.test.ts +522 -0
- package/tsconfig.json +26 -0
- package/vitest.config.ts +16 -0
- package/vitest.setup.ts +12 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider Factory Unit Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for the embedding provider factory module ensuring correct
|
|
5
|
+
* provider creation with appropriate baseURL mapping.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Effect, Option, Redacted } from 'effect'
|
|
9
|
+
import { describe, expect, it } from 'vitest'
|
|
10
|
+
import {
|
|
11
|
+
createEmbeddingProviderDirect,
|
|
12
|
+
getProviderBaseURL,
|
|
13
|
+
PROVIDER_BASE_URLS,
|
|
14
|
+
} from './provider-factory.js'
|
|
15
|
+
|
|
16
|
+
describe('Provider Factory', () => {
|
|
17
|
+
describe('PROVIDER_BASE_URLS constant', () => {
|
|
18
|
+
it('should have undefined for openai (uses SDK default)', () => {
|
|
19
|
+
expect(PROVIDER_BASE_URLS.openai).toBeUndefined()
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('should have correct URL for ollama', () => {
|
|
23
|
+
expect(PROVIDER_BASE_URLS.ollama).toBe('http://localhost:11434/v1')
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('should have correct URL for lm-studio', () => {
|
|
27
|
+
expect(PROVIDER_BASE_URLS['lm-studio']).toBe('http://localhost:1234/v1')
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('should have correct URL for openrouter', () => {
|
|
31
|
+
expect(PROVIDER_BASE_URLS.openrouter).toBe('https://openrouter.ai/api/v1')
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('should have all five providers defined', () => {
|
|
35
|
+
const providers = Object.keys(PROVIDER_BASE_URLS)
|
|
36
|
+
expect(providers).toHaveLength(5)
|
|
37
|
+
expect(providers).toContain('openai')
|
|
38
|
+
expect(providers).toContain('ollama')
|
|
39
|
+
expect(providers).toContain('lm-studio')
|
|
40
|
+
expect(providers).toContain('openrouter')
|
|
41
|
+
expect(providers).toContain('voyage')
|
|
42
|
+
})
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
describe('getProviderBaseURL', () => {
|
|
46
|
+
it('should return config baseURL when Option.some is provided', () => {
|
|
47
|
+
const customURL = 'https://custom.api.com/v1'
|
|
48
|
+
const result = getProviderBaseURL('openai', Option.some(customURL))
|
|
49
|
+
expect(result).toBe(customURL)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('should return config baseURL over provider default', () => {
|
|
53
|
+
const customURL = 'https://custom-ollama.example.com/v1'
|
|
54
|
+
const result = getProviderBaseURL('ollama', Option.some(customURL))
|
|
55
|
+
expect(result).toBe(customURL)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('should return provider default when Option.none for ollama', () => {
|
|
59
|
+
const result = getProviderBaseURL('ollama', Option.none())
|
|
60
|
+
expect(result).toBe('http://localhost:11434/v1')
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('should return provider default when Option.none for lm-studio', () => {
|
|
64
|
+
const result = getProviderBaseURL('lm-studio', Option.none())
|
|
65
|
+
expect(result).toBe('http://localhost:1234/v1')
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('should return provider default when Option.none for openrouter', () => {
|
|
69
|
+
const result = getProviderBaseURL('openrouter', Option.none())
|
|
70
|
+
expect(result).toBe('https://openrouter.ai/api/v1')
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('should return undefined for openai when Option.none', () => {
|
|
74
|
+
const result = getProviderBaseURL('openai', Option.none())
|
|
75
|
+
expect(result).toBeUndefined()
|
|
76
|
+
})
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
describe('createEmbeddingProviderDirect', () => {
|
|
80
|
+
it('should create provider with ollama default baseURL', async () => {
|
|
81
|
+
const program = createEmbeddingProviderDirect({
|
|
82
|
+
provider: 'ollama',
|
|
83
|
+
apiKey: 'test-key',
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
const provider = await Effect.runPromise(program)
|
|
87
|
+
expect(provider.name).toContain('ollama')
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('should create provider with lm-studio default baseURL', async () => {
|
|
91
|
+
const program = createEmbeddingProviderDirect({
|
|
92
|
+
provider: 'lm-studio',
|
|
93
|
+
apiKey: 'test-key',
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
const provider = await Effect.runPromise(program)
|
|
97
|
+
expect(provider.name).toContain('lm-studio')
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('should create provider with openrouter default baseURL', async () => {
|
|
101
|
+
const program = createEmbeddingProviderDirect({
|
|
102
|
+
provider: 'openrouter',
|
|
103
|
+
apiKey: 'test-key',
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
const provider = await Effect.runPromise(program)
|
|
107
|
+
expect(provider.name).toContain('openrouter')
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('should create openai provider with SDK default (no baseURL)', async () => {
|
|
111
|
+
const program = createEmbeddingProviderDirect({
|
|
112
|
+
provider: 'openai',
|
|
113
|
+
apiKey: 'test-key',
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
const provider = await Effect.runPromise(program)
|
|
117
|
+
expect(provider.name).toContain('openai')
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('should respect custom baseURL override for any provider', async () => {
|
|
121
|
+
const customURL = 'https://my-proxy.example.com/v1'
|
|
122
|
+
const program = createEmbeddingProviderDirect({
|
|
123
|
+
provider: 'openai',
|
|
124
|
+
baseURL: customURL,
|
|
125
|
+
apiKey: 'test-key',
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
const provider = await Effect.runPromise(program)
|
|
129
|
+
expect(provider).toBeDefined()
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('should accept baseURL as Option.some', async () => {
|
|
133
|
+
const customURL = 'https://custom.api.com/v1'
|
|
134
|
+
const program = createEmbeddingProviderDirect({
|
|
135
|
+
provider: 'openai',
|
|
136
|
+
baseURL: Option.some(customURL),
|
|
137
|
+
apiKey: 'test-key',
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
const provider = await Effect.runPromise(program)
|
|
141
|
+
expect(provider).toBeDefined()
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it('should use provider default when baseURL is Option.none', async () => {
|
|
145
|
+
const program = createEmbeddingProviderDirect({
|
|
146
|
+
provider: 'ollama',
|
|
147
|
+
baseURL: Option.none(),
|
|
148
|
+
apiKey: 'test-key',
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
const provider = await Effect.runPromise(program)
|
|
152
|
+
expect(provider.name).toContain('ollama')
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
it('should pass custom model to provider', async () => {
|
|
156
|
+
const program = createEmbeddingProviderDirect({
|
|
157
|
+
provider: 'openai',
|
|
158
|
+
model: 'text-embedding-3-large',
|
|
159
|
+
apiKey: 'test-key',
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
const provider = await Effect.runPromise(program)
|
|
163
|
+
expect(provider.name).toContain('text-embedding-3-large')
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('should accept apiKey as Option.some', async () => {
|
|
167
|
+
const program = createEmbeddingProviderDirect({
|
|
168
|
+
provider: 'openai',
|
|
169
|
+
apiKey: Option.some('test-key'),
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
const provider = await Effect.runPromise(program)
|
|
173
|
+
expect(provider).toBeDefined()
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
it('should fail when no API key provided', async () => {
|
|
177
|
+
const originalEnv = process.env.OPENAI_API_KEY
|
|
178
|
+
delete process.env.OPENAI_API_KEY
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
const program = createEmbeddingProviderDirect({
|
|
182
|
+
provider: 'openai',
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
await expect(Effect.runPromise(program)).rejects.toThrow()
|
|
186
|
+
} finally {
|
|
187
|
+
if (originalEnv) {
|
|
188
|
+
process.env.OPENAI_API_KEY = originalEnv
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
it('should fail when apiKey is Option.none and no env var', async () => {
|
|
194
|
+
const originalEnv = process.env.OPENAI_API_KEY
|
|
195
|
+
delete process.env.OPENAI_API_KEY
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
const program = createEmbeddingProviderDirect({
|
|
199
|
+
provider: 'openai',
|
|
200
|
+
apiKey: Option.none(),
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
await expect(Effect.runPromise(program)).rejects.toThrow()
|
|
204
|
+
} finally {
|
|
205
|
+
if (originalEnv) {
|
|
206
|
+
process.env.OPENAI_API_KEY = originalEnv
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
it('should accept apiKey as Redacted<string>', async () => {
|
|
212
|
+
const redactedKey = Redacted.make('test-redacted-key')
|
|
213
|
+
const program = createEmbeddingProviderDirect({
|
|
214
|
+
provider: 'openai',
|
|
215
|
+
apiKey: redactedKey,
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
const provider = await Effect.runPromise(program)
|
|
219
|
+
expect(provider).toBeDefined()
|
|
220
|
+
expect(provider.name).toContain('openai')
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
it('should not expose Redacted API key when stringified', () => {
|
|
224
|
+
const redactedKey = Redacted.make('sk-secret-key-12345')
|
|
225
|
+
const stringified = String(redactedKey)
|
|
226
|
+
|
|
227
|
+
// Redacted type shows <redacted> when stringified
|
|
228
|
+
expect(stringified).not.toContain('sk-secret-key-12345')
|
|
229
|
+
expect(stringified).toContain('<redacted>')
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
it('should preserve API key value in Redacted wrapper', () => {
|
|
233
|
+
const secretKey = 'sk-secret-key-12345'
|
|
234
|
+
const redactedKey = Redacted.make(secretKey)
|
|
235
|
+
|
|
236
|
+
// Value can still be extracted when needed
|
|
237
|
+
expect(Redacted.value(redactedKey)).toBe(secretKey)
|
|
238
|
+
})
|
|
239
|
+
})
|
|
240
|
+
})
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Embedding Provider Factory
|
|
3
|
+
*
|
|
4
|
+
* Creates embedding providers based on configuration. Supports multiple
|
|
5
|
+
* providers (OpenAI, Ollama, LM Studio, OpenRouter) with automatic
|
|
6
|
+
* baseURL mapping for OpenAI-compatible APIs.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { Effect, Option, Redacted } from 'effect'
|
|
10
|
+
import { ConfigService, type EmbeddingProvider } from '../config/index.js'
|
|
11
|
+
import type { EmbeddingsConfig } from '../config/schema.js'
|
|
12
|
+
import type { ApiKeyMissingError } from '../errors/index.js'
|
|
13
|
+
import { createOpenAIProvider } from './openai-provider.js'
|
|
14
|
+
import { PROVIDER_BASE_URLS } from './provider-constants.js'
|
|
15
|
+
import type { EmbeddingProvider as EmbeddingProviderInterface } from './types.js'
|
|
16
|
+
import { createVoyageProvider } from './voyage-provider.js'
|
|
17
|
+
|
|
18
|
+
// Re-export provider constants for backward compatibility
|
|
19
|
+
export { PROVIDER_BASE_URLS } from './provider-constants.js'
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Get the base URL for a provider, respecting config override.
|
|
23
|
+
*
|
|
24
|
+
* Precedence:
|
|
25
|
+
* 1. Explicit baseURL from config (highest priority)
|
|
26
|
+
* 2. Provider-specific default from PROVIDER_BASE_URLS
|
|
27
|
+
* 3. OpenAI SDK default (undefined means use SDK default)
|
|
28
|
+
*/
|
|
29
|
+
export const getProviderBaseURL = (
|
|
30
|
+
provider: EmbeddingProvider,
|
|
31
|
+
configBaseURL: Option.Option<string>,
|
|
32
|
+
): string | undefined => {
|
|
33
|
+
// Config baseURL takes precedence
|
|
34
|
+
if (Option.isSome(configBaseURL)) {
|
|
35
|
+
return configBaseURL.value
|
|
36
|
+
}
|
|
37
|
+
// Fall back to provider default
|
|
38
|
+
return PROVIDER_BASE_URLS[provider]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ============================================================================
|
|
42
|
+
// Provider Factory
|
|
43
|
+
// ============================================================================
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Configuration subset needed for provider creation.
|
|
47
|
+
* Extracted from EmbeddingsConfig to allow direct config passing.
|
|
48
|
+
*/
|
|
49
|
+
export interface ProviderFactoryConfig {
|
|
50
|
+
readonly provider: EmbeddingProvider
|
|
51
|
+
readonly baseURL?: Option.Option<string> | string | undefined
|
|
52
|
+
readonly model?: string | undefined
|
|
53
|
+
readonly dimensions?: number | undefined
|
|
54
|
+
readonly batchSize?: number | undefined
|
|
55
|
+
/**
|
|
56
|
+
* API key for the provider. Accepts multiple formats:
|
|
57
|
+
* - Plain string: 'sk-...'
|
|
58
|
+
* - Redacted<string>: Redacted.make('sk-...') (recommended for security)
|
|
59
|
+
* - Option<string>: Option.some('sk-...')
|
|
60
|
+
*/
|
|
61
|
+
readonly apiKey?:
|
|
62
|
+
| Option.Option<string>
|
|
63
|
+
| string
|
|
64
|
+
| Redacted.Redacted<string>
|
|
65
|
+
| undefined
|
|
66
|
+
/**
|
|
67
|
+
* Request timeout in milliseconds.
|
|
68
|
+
* Default: 30000 (30 seconds)
|
|
69
|
+
*/
|
|
70
|
+
readonly timeout?: number | undefined
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Normalize baseURL from various input formats to Option<string>.
|
|
75
|
+
*/
|
|
76
|
+
const normalizeBaseURL = (
|
|
77
|
+
baseURL: Option.Option<string> | string | undefined,
|
|
78
|
+
): Option.Option<string> => {
|
|
79
|
+
if (baseURL === undefined) {
|
|
80
|
+
return Option.none()
|
|
81
|
+
}
|
|
82
|
+
if (typeof baseURL === 'string') {
|
|
83
|
+
return Option.some(baseURL)
|
|
84
|
+
}
|
|
85
|
+
return baseURL
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Normalize apiKey from various input formats to string | Redacted<string> | undefined.
|
|
90
|
+
* Preserves Redacted wrapper for security - don't unwrap until needed.
|
|
91
|
+
*/
|
|
92
|
+
const normalizeApiKey = (
|
|
93
|
+
apiKey:
|
|
94
|
+
| Option.Option<string>
|
|
95
|
+
| string
|
|
96
|
+
| Redacted.Redacted<string>
|
|
97
|
+
| undefined,
|
|
98
|
+
): string | Redacted.Redacted<string> | undefined => {
|
|
99
|
+
if (apiKey === undefined) {
|
|
100
|
+
return undefined
|
|
101
|
+
}
|
|
102
|
+
// Check for Redacted first (keep wrapped for security)
|
|
103
|
+
if (Redacted.isRedacted(apiKey)) {
|
|
104
|
+
return apiKey
|
|
105
|
+
}
|
|
106
|
+
if (typeof apiKey === 'string') {
|
|
107
|
+
return apiKey
|
|
108
|
+
}
|
|
109
|
+
// Handle Option<string>
|
|
110
|
+
return Option.isSome(apiKey) ? apiKey.value : undefined
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Create an embedding provider based on configuration.
|
|
115
|
+
*
|
|
116
|
+
* All supported providers (OpenAI, Ollama, LM Studio, OpenRouter) use
|
|
117
|
+
* OpenAI-compatible APIs, so we use the OpenAI provider with different
|
|
118
|
+
* base URLs.
|
|
119
|
+
*
|
|
120
|
+
* @param config - Optional explicit config (if not provided, reads from ConfigService)
|
|
121
|
+
* @returns Effect yielding the configured EmbeddingProvider
|
|
122
|
+
*
|
|
123
|
+
* @example
|
|
124
|
+
* ```typescript
|
|
125
|
+
* // Using ConfigService (reads from environment/config file)
|
|
126
|
+
* const provider = yield* createEmbeddingProvider()
|
|
127
|
+
*
|
|
128
|
+
* // Explicit config override
|
|
129
|
+
* const provider = yield* createEmbeddingProvider({
|
|
130
|
+
* provider: 'ollama',
|
|
131
|
+
* model: 'nomic-embed-text',
|
|
132
|
+
* })
|
|
133
|
+
* ```
|
|
134
|
+
*/
|
|
135
|
+
export const createEmbeddingProvider = (
|
|
136
|
+
config?: ProviderFactoryConfig,
|
|
137
|
+
): Effect.Effect<
|
|
138
|
+
EmbeddingProviderInterface,
|
|
139
|
+
ApiKeyMissingError,
|
|
140
|
+
ConfigService
|
|
141
|
+
> =>
|
|
142
|
+
Effect.gen(function* () {
|
|
143
|
+
// Get embeddings config from service if not provided
|
|
144
|
+
const embeddingsConfig: EmbeddingsConfig = config
|
|
145
|
+
? ({
|
|
146
|
+
provider: config.provider,
|
|
147
|
+
baseURL: normalizeBaseURL(config.baseURL),
|
|
148
|
+
model: config.model ?? 'text-embedding-3-small',
|
|
149
|
+
dimensions: config.dimensions, // Let provider determine default if not specified
|
|
150
|
+
batchSize: config.batchSize ?? 100,
|
|
151
|
+
maxRetries: 3,
|
|
152
|
+
retryDelayMs: 1000,
|
|
153
|
+
timeoutMs: 30000,
|
|
154
|
+
apiKey: Option.fromNullable(normalizeApiKey(config.apiKey)),
|
|
155
|
+
} as EmbeddingsConfig)
|
|
156
|
+
: (yield* ConfigService).embeddings
|
|
157
|
+
|
|
158
|
+
const provider = embeddingsConfig.provider
|
|
159
|
+
const baseURL = getProviderBaseURL(provider, embeddingsConfig.baseURL)
|
|
160
|
+
|
|
161
|
+
// Extract API key from config if available
|
|
162
|
+
const apiKey = Option.isSome(embeddingsConfig.apiKey)
|
|
163
|
+
? embeddingsConfig.apiKey.value
|
|
164
|
+
: undefined
|
|
165
|
+
|
|
166
|
+
// Voyage AI uses its own native API, not OpenAI-compatible
|
|
167
|
+
if (provider === 'voyage') {
|
|
168
|
+
return yield* createVoyageProvider({
|
|
169
|
+
model: embeddingsConfig.model,
|
|
170
|
+
batchSize: embeddingsConfig.batchSize,
|
|
171
|
+
apiKey,
|
|
172
|
+
timeout: embeddingsConfig.timeoutMs,
|
|
173
|
+
})
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// All other providers use OpenAI-compatible API
|
|
177
|
+
return yield* createOpenAIProvider({
|
|
178
|
+
model: embeddingsConfig.model,
|
|
179
|
+
dimensions: embeddingsConfig.dimensions,
|
|
180
|
+
batchSize: embeddingsConfig.batchSize,
|
|
181
|
+
baseURL,
|
|
182
|
+
apiKey,
|
|
183
|
+
timeout: embeddingsConfig.timeoutMs,
|
|
184
|
+
})
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Create an embedding provider without ConfigService dependency.
|
|
189
|
+
*
|
|
190
|
+
* Use this when you need to create a provider outside of the Effect
|
|
191
|
+
* context or when you have explicit config values.
|
|
192
|
+
*
|
|
193
|
+
* @param config - Explicit provider configuration
|
|
194
|
+
* @returns Effect yielding the configured EmbeddingProvider
|
|
195
|
+
*/
|
|
196
|
+
export const createEmbeddingProviderDirect = (
|
|
197
|
+
config: ProviderFactoryConfig,
|
|
198
|
+
): Effect.Effect<EmbeddingProviderInterface, ApiKeyMissingError> =>
|
|
199
|
+
Effect.gen(function* () {
|
|
200
|
+
const provider = config.provider
|
|
201
|
+
const baseURL = getProviderBaseURL(
|
|
202
|
+
provider,
|
|
203
|
+
normalizeBaseURL(config.baseURL),
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
// Voyage AI uses its own native API, not OpenAI-compatible
|
|
207
|
+
if (provider === 'voyage') {
|
|
208
|
+
return yield* createVoyageProvider({
|
|
209
|
+
model: config.model,
|
|
210
|
+
batchSize: config.batchSize,
|
|
211
|
+
apiKey: normalizeApiKey(config.apiKey),
|
|
212
|
+
timeout: config.timeout,
|
|
213
|
+
})
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// All other providers use OpenAI-compatible API
|
|
217
|
+
return yield* createOpenAIProvider({
|
|
218
|
+
model: config.model,
|
|
219
|
+
dimensions: config.dimensions,
|
|
220
|
+
batchSize: config.batchSize,
|
|
221
|
+
baseURL,
|
|
222
|
+
apiKey: normalizeApiKey(config.apiKey),
|
|
223
|
+
timeout: config.timeout,
|
|
224
|
+
})
|
|
225
|
+
})
|