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.
Files changed (337) hide show
  1. package/.changeset/README.md +28 -0
  2. package/.changeset/config.json +11 -0
  3. package/.claude/settings.local.json +25 -0
  4. package/.github/workflows/ci.yml +83 -0
  5. package/.github/workflows/claude-code-review.yml +44 -0
  6. package/.github/workflows/claude.yml +85 -0
  7. package/.github/workflows/release.yml +113 -0
  8. package/.tldrignore +112 -0
  9. package/BACKLOG.md +338 -0
  10. package/CONTRIBUTING.md +186 -0
  11. package/NOTES/NOTES +44 -0
  12. package/README.md +434 -11
  13. package/biome.json +36 -0
  14. package/cspell.config.yaml +14 -0
  15. package/dist/chunk-23UPXDNL.js +3044 -0
  16. package/dist/chunk-2W7MO2DL.js +1366 -0
  17. package/dist/chunk-3NUAZGMA.js +1689 -0
  18. package/dist/chunk-7TOWB2XB.js +366 -0
  19. package/dist/chunk-7XOTOADQ.js +3065 -0
  20. package/dist/chunk-AH2PDM2K.js +3042 -0
  21. package/dist/chunk-BNXWSZ63.js +3742 -0
  22. package/dist/chunk-BTL5DJVU.js +3222 -0
  23. package/dist/chunk-HDHYG7E4.js +104 -0
  24. package/dist/chunk-HLR4KZBP.js +3234 -0
  25. package/dist/chunk-IP3FRFEB.js +1045 -0
  26. package/dist/chunk-KHU56VDO.js +3042 -0
  27. package/dist/chunk-KRYIFLQR.js +88 -0
  28. package/dist/chunk-LBSDNLEM.js +287 -0
  29. package/dist/chunk-MNTQ7HCP.js +2643 -0
  30. package/dist/chunk-MUJELQQ6.js +1387 -0
  31. package/dist/chunk-MXJGMSLV.js +2199 -0
  32. package/dist/chunk-N6QJGC3Z.js +2636 -0
  33. package/dist/chunk-OBELGBPM.js +1713 -0
  34. package/dist/chunk-OT7R5XTA.js +3192 -0
  35. package/dist/chunk-P7X4RA2T.js +106 -0
  36. package/dist/chunk-PIDUQNC2.js +3185 -0
  37. package/dist/chunk-POGCDIH4.js +3187 -0
  38. package/dist/chunk-PSIEOQGZ.js +3043 -0
  39. package/dist/chunk-PVRT3IHA.js +3238 -0
  40. package/dist/chunk-QNN4TT23.js +1430 -0
  41. package/dist/chunk-RE3R45RJ.js +3042 -0
  42. package/dist/chunk-S7E6TFX6.js +803 -0
  43. package/dist/chunk-SG6GLU4U.js +1378 -0
  44. package/dist/chunk-SJCDV2ST.js +274 -0
  45. package/dist/chunk-SYE5XLF3.js +104 -0
  46. package/dist/chunk-T5VLYBZD.js +103 -0
  47. package/dist/chunk-TOQB7VWU.js +3238 -0
  48. package/dist/chunk-VFNMZ4ZQ.js +3228 -0
  49. package/dist/chunk-VVTGZNBT.js +1629 -0
  50. package/dist/chunk-W7Q4RFEV.js +104 -0
  51. package/dist/chunk-XTYYVRLO.js +3190 -0
  52. package/dist/chunk-Y6MDYVJD.js +3063 -0
  53. package/dist/cli/main.d.ts +1 -0
  54. package/dist/cli/main.js +5458 -0
  55. package/dist/index.d.ts +653 -0
  56. package/dist/index.js +79 -0
  57. package/dist/mcp/server.d.ts +1 -0
  58. package/dist/mcp/server.js +472 -0
  59. package/dist/schema-BAWSG7KY.js +22 -0
  60. package/dist/schema-E3QUPL26.js +20 -0
  61. package/dist/schema-EHL7WUT6.js +20 -0
  62. package/docs/019-USAGE.md +625 -0
  63. package/docs/020-current-implementation.md +364 -0
  64. package/docs/021-DOGFOODING-FINDINGS.md +175 -0
  65. package/docs/BACKLOG.md +80 -0
  66. package/docs/CONFIG.md +1123 -0
  67. package/docs/DESIGN.md +439 -0
  68. package/docs/ERRORS.md +383 -0
  69. package/docs/PROJECT.md +88 -0
  70. package/docs/ROADMAP.md +407 -0
  71. package/docs/summarization.md +320 -0
  72. package/docs/test-links.md +9 -0
  73. package/justfile +40 -0
  74. package/package.json +74 -9
  75. package/pnpm-workspace.yaml +5 -0
  76. package/research/INDEX.md +315 -0
  77. package/research/code-review/README.md +90 -0
  78. package/research/code-review/cli-error-handling-review.md +979 -0
  79. package/research/code-review/code-review-validation-report.md +464 -0
  80. package/research/code-review/main-ts-review.md +1128 -0
  81. package/research/config-analysis/01-current-implementation.md +470 -0
  82. package/research/config-analysis/02-strategy-recommendation.md +428 -0
  83. package/research/config-analysis/03-task-candidates.md +715 -0
  84. package/research/config-analysis/033-research-configuration-management.md +828 -0
  85. package/research/config-analysis/034-research-effect-cli-config.md +1504 -0
  86. package/research/config-analysis/04-consolidated-task-candidates.md +277 -0
  87. package/research/config-docs/SUMMARY.md +357 -0
  88. package/research/config-docs/TEST-RESULTS.md +776 -0
  89. package/research/config-docs/TODO.md +542 -0
  90. package/research/config-docs/analysis.md +744 -0
  91. package/research/config-docs/fix-validation.md +502 -0
  92. package/research/config-docs/help-audit.md +264 -0
  93. package/research/config-docs/help-system-analysis.md +890 -0
  94. package/research/dogfood/consolidated-tool-evaluation.md +373 -0
  95. package/research/dogfood/strategy-a/a-synthesis.md +184 -0
  96. package/research/dogfood/strategy-a/a1-docs.md +226 -0
  97. package/research/dogfood/strategy-a/a2-amorphic.md +156 -0
  98. package/research/dogfood/strategy-a/a3-llm.md +164 -0
  99. package/research/dogfood/strategy-b/b-synthesis.md +228 -0
  100. package/research/dogfood/strategy-b/b1-architecture.md +207 -0
  101. package/research/dogfood/strategy-b/b2-gaps.md +258 -0
  102. package/research/dogfood/strategy-b/b3-workflows.md +250 -0
  103. package/research/dogfood/strategy-c/c-synthesis.md +451 -0
  104. package/research/dogfood/strategy-c/c1-explorer.md +192 -0
  105. package/research/dogfood/strategy-c/c2-diver-memory.md +145 -0
  106. package/research/dogfood/strategy-c/c3-diver-control.md +148 -0
  107. package/research/dogfood/strategy-c/c4-diver-failure.md +151 -0
  108. package/research/dogfood/strategy-c/c5-diver-execution.md +221 -0
  109. package/research/dogfood/strategy-c/c6-diver-org.md +221 -0
  110. package/research/effect-cli-error-handling.md +845 -0
  111. package/research/effect-errors-as-values.md +943 -0
  112. package/research/errors-task-analysis/00-consolidated-tasks.md +207 -0
  113. package/research/errors-task-analysis/cli-commands-analysis.md +909 -0
  114. package/research/errors-task-analysis/embeddings-analysis.md +709 -0
  115. package/research/errors-task-analysis/index-search-analysis.md +812 -0
  116. package/research/frontmatter/COMMENTS-ARE-SKIPPED.md +149 -0
  117. package/research/frontmatter/LLM-CODE-NAVIGATION.md +276 -0
  118. package/research/issue-review.md +603 -0
  119. package/research/llm-summarization/agent-cli-tools-2026.md +1082 -0
  120. package/research/llm-summarization/alternative-providers-2026.md +1428 -0
  121. package/research/llm-summarization/anthropic-2026.md +367 -0
  122. package/research/llm-summarization/claude-cli-integration.md +1706 -0
  123. package/research/llm-summarization/cli-integration-patterns.md +3155 -0
  124. package/research/llm-summarization/openai-2026.md +473 -0
  125. package/research/llm-summarization/openai-compatible-providers-2026.md +1022 -0
  126. package/research/llm-summarization/opencode-cli-integration.md +1552 -0
  127. package/research/llm-summarization/prompt-engineering-2026.md +1426 -0
  128. package/research/llm-summarization/prototype-results.md +56 -0
  129. package/research/llm-summarization/provider-switching-patterns-2026.md +2153 -0
  130. package/research/llm-summarization/typescript-llm-libraries-2026.md +2436 -0
  131. package/research/mdcontext-error-analysis.md +521 -0
  132. package/research/mdcontext-pudding/00-EXECUTIVE-SUMMARY.md +282 -0
  133. package/research/mdcontext-pudding/01-index-embed.md +956 -0
  134. package/research/mdcontext-pudding/02-search-COMMANDS.md +142 -0
  135. package/research/mdcontext-pudding/02-search-SUMMARY.md +146 -0
  136. package/research/mdcontext-pudding/02-search.md +970 -0
  137. package/research/mdcontext-pudding/03-context.md +779 -0
  138. package/research/mdcontext-pudding/04-navigation-and-analytics.md +803 -0
  139. package/research/mdcontext-pudding/04-tree.md +704 -0
  140. package/research/mdcontext-pudding/05-config.md +1038 -0
  141. package/research/mdcontext-pudding/06-links-summary.txt +87 -0
  142. package/research/mdcontext-pudding/06-links.md +679 -0
  143. package/research/mdcontext-pudding/07-stats.md +693 -0
  144. package/research/mdcontext-pudding/BUG-FIX-PLAN.md +388 -0
  145. package/research/mdcontext-pudding/P0-BUG-VALIDATION.md +167 -0
  146. package/research/mdcontext-pudding/README.md +168 -0
  147. package/research/mdcontext-pudding/TESTING-SUMMARY.md +128 -0
  148. package/research/npm_publish/011-npm-workflow-research-agent2.md +792 -0
  149. package/research/npm_publish/012-npm-workflow-research-agent1.md +530 -0
  150. package/research/npm_publish/013-npm-workflow-research-agent3.md +722 -0
  151. package/research/npm_publish/014-npm-workflow-synthesis.md +556 -0
  152. package/research/npm_publish/031-npm-workflow-task-analysis.md +134 -0
  153. package/research/research-quality-review.md +834 -0
  154. package/research/semantic-search/002-research-embedding-models.md +490 -0
  155. package/research/semantic-search/003-research-rag-alternatives.md +523 -0
  156. package/research/semantic-search/004-research-vector-search.md +841 -0
  157. package/research/semantic-search/032-research-semantic-search.md +427 -0
  158. package/research/semantic-search/embedding-text-analysis.md +156 -0
  159. package/research/semantic-search/multi-word-failure-reproduction.md +171 -0
  160. package/research/semantic-search/query-processing-analysis.md +207 -0
  161. package/research/semantic-search/root-cause-and-solution.md +114 -0
  162. package/research/semantic-search/threshold-validation-report.md +69 -0
  163. package/research/semantic-search/vector-search-analysis.md +63 -0
  164. package/research/task-management-2026/00-synthesis-recommendations.md +295 -0
  165. package/research/task-management-2026/01-ai-workflow-tools.md +416 -0
  166. package/research/task-management-2026/02-agent-framework-patterns.md +476 -0
  167. package/research/task-management-2026/03-lightweight-file-based.md +567 -0
  168. package/research/task-management-2026/04-established-tools-ai-features.md +541 -0
  169. package/research/task-management-2026/linear/01-core-features-workflow.md +771 -0
  170. package/research/task-management-2026/linear/02-api-integrations.md +930 -0
  171. package/research/task-management-2026/linear/03-ai-features.md +368 -0
  172. package/research/task-management-2026/linear/04-pricing-setup.md +205 -0
  173. package/research/task-management-2026/linear/05-usage-patterns-best-practices.md +605 -0
  174. package/research/test-path-issues.md +276 -0
  175. package/review/ALP-76/1-error-type-design.md +962 -0
  176. package/review/ALP-76/2-error-handling-patterns.md +906 -0
  177. package/review/ALP-76/3-error-presentation.md +624 -0
  178. package/review/ALP-76/4-test-coverage.md +625 -0
  179. package/review/ALP-76/5-migration-completeness.md +440 -0
  180. package/review/ALP-76/6-effect-best-practices.md +755 -0
  181. package/scripts/apply-branch-protection.sh +47 -0
  182. package/scripts/branch-protection-templates.json +79 -0
  183. package/scripts/prototype-summarization.ts +346 -0
  184. package/scripts/rebuild-hnswlib.js +58 -0
  185. package/scripts/setup-branch-protection.sh +64 -0
  186. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/active-provider.json +7 -0
  187. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/bm25.json +541 -0
  188. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/bm25.meta.json +5 -0
  189. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/config.json +8 -0
  190. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/embeddings/openai_text-embedding-3-small_512/vectors.bin +0 -0
  191. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/embeddings/openai_text-embedding-3-small_512/vectors.meta.bin +0 -0
  192. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/indexes/documents.json +60 -0
  193. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/indexes/links.json +13 -0
  194. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/indexes/sections.json +1197 -0
  195. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/configuration-management.md +99 -0
  196. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/distributed-systems.md +92 -0
  197. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/error-handling.md +78 -0
  198. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/failure-automation.md +55 -0
  199. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/job-context.md +69 -0
  200. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/process-orchestration.md +99 -0
  201. package/src/cli/argv-preprocessor.test.ts +210 -0
  202. package/src/cli/argv-preprocessor.ts +202 -0
  203. package/src/cli/cli.test.ts +627 -0
  204. package/src/cli/commands/backlinks.ts +54 -0
  205. package/src/cli/commands/config-cmd.ts +642 -0
  206. package/src/cli/commands/context.ts +285 -0
  207. package/src/cli/commands/duplicates.ts +122 -0
  208. package/src/cli/commands/embeddings.ts +529 -0
  209. package/src/cli/commands/index-cmd.ts +480 -0
  210. package/src/cli/commands/index.ts +16 -0
  211. package/src/cli/commands/links.ts +52 -0
  212. package/src/cli/commands/search.ts +1281 -0
  213. package/src/cli/commands/stats.ts +149 -0
  214. package/src/cli/commands/tree.ts +128 -0
  215. package/src/cli/config-layer.ts +176 -0
  216. package/src/cli/error-handler.test.ts +235 -0
  217. package/src/cli/error-handler.ts +655 -0
  218. package/src/cli/flag-schemas.ts +341 -0
  219. package/src/cli/help.ts +588 -0
  220. package/src/cli/index.ts +9 -0
  221. package/src/cli/main.ts +435 -0
  222. package/src/cli/options.ts +41 -0
  223. package/src/cli/shared-error-handling.ts +199 -0
  224. package/src/cli/typo-suggester.test.ts +105 -0
  225. package/src/cli/typo-suggester.ts +130 -0
  226. package/src/cli/utils.ts +259 -0
  227. package/src/config/file-provider.test.ts +320 -0
  228. package/src/config/file-provider.ts +273 -0
  229. package/src/config/index.ts +72 -0
  230. package/src/config/integration.test.ts +667 -0
  231. package/src/config/precedence.test.ts +277 -0
  232. package/src/config/precedence.ts +451 -0
  233. package/src/config/schema.test.ts +414 -0
  234. package/src/config/schema.ts +603 -0
  235. package/src/config/service.test.ts +320 -0
  236. package/src/config/service.ts +243 -0
  237. package/src/config/testing.test.ts +264 -0
  238. package/src/config/testing.ts +110 -0
  239. package/src/core/index.ts +1 -0
  240. package/src/core/types.ts +113 -0
  241. package/src/duplicates/detector.test.ts +183 -0
  242. package/src/duplicates/detector.ts +414 -0
  243. package/src/duplicates/index.ts +18 -0
  244. package/src/embeddings/embedding-namespace.test.ts +300 -0
  245. package/src/embeddings/embedding-namespace.ts +947 -0
  246. package/src/embeddings/heading-boost.test.ts +222 -0
  247. package/src/embeddings/hnsw-build-options.test.ts +198 -0
  248. package/src/embeddings/hyde.test.ts +272 -0
  249. package/src/embeddings/hyde.ts +264 -0
  250. package/src/embeddings/index.ts +10 -0
  251. package/src/embeddings/openai-provider.ts +414 -0
  252. package/src/embeddings/pricing.json +22 -0
  253. package/src/embeddings/provider-constants.ts +204 -0
  254. package/src/embeddings/provider-errors.test.ts +967 -0
  255. package/src/embeddings/provider-errors.ts +565 -0
  256. package/src/embeddings/provider-factory.test.ts +240 -0
  257. package/src/embeddings/provider-factory.ts +225 -0
  258. package/src/embeddings/provider-integration.test.ts +788 -0
  259. package/src/embeddings/query-preprocessing.test.ts +187 -0
  260. package/src/embeddings/semantic-search-threshold.test.ts +508 -0
  261. package/src/embeddings/semantic-search.ts +1270 -0
  262. package/src/embeddings/types.ts +359 -0
  263. package/src/embeddings/vector-store.ts +708 -0
  264. package/src/embeddings/voyage-provider.ts +313 -0
  265. package/src/errors/errors.test.ts +845 -0
  266. package/src/errors/index.ts +533 -0
  267. package/src/index/ignore-patterns.test.ts +354 -0
  268. package/src/index/ignore-patterns.ts +305 -0
  269. package/src/index/index.ts +4 -0
  270. package/src/index/indexer.ts +684 -0
  271. package/src/index/storage.ts +260 -0
  272. package/src/index/types.ts +147 -0
  273. package/src/index/watcher.ts +189 -0
  274. package/src/index.ts +30 -0
  275. package/src/integration/search-keyword.test.ts +678 -0
  276. package/src/mcp/server.ts +612 -0
  277. package/src/parser/index.ts +1 -0
  278. package/src/parser/parser.test.ts +291 -0
  279. package/src/parser/parser.ts +394 -0
  280. package/src/parser/section-filter.test.ts +277 -0
  281. package/src/parser/section-filter.ts +392 -0
  282. package/src/search/__tests__/hybrid-search.test.ts +650 -0
  283. package/src/search/bm25-store.ts +366 -0
  284. package/src/search/cross-encoder.test.ts +253 -0
  285. package/src/search/cross-encoder.ts +406 -0
  286. package/src/search/fuzzy-search.test.ts +419 -0
  287. package/src/search/fuzzy-search.ts +273 -0
  288. package/src/search/hybrid-search.ts +448 -0
  289. package/src/search/path-matcher.test.ts +276 -0
  290. package/src/search/path-matcher.ts +33 -0
  291. package/src/search/query-parser.test.ts +260 -0
  292. package/src/search/query-parser.ts +319 -0
  293. package/src/search/searcher.test.ts +280 -0
  294. package/src/search/searcher.ts +724 -0
  295. package/src/search/wink-bm25.d.ts +30 -0
  296. package/src/summarization/cli-providers/claude.ts +202 -0
  297. package/src/summarization/cli-providers/detection.test.ts +273 -0
  298. package/src/summarization/cli-providers/detection.ts +118 -0
  299. package/src/summarization/cli-providers/index.ts +8 -0
  300. package/src/summarization/cost.test.ts +139 -0
  301. package/src/summarization/cost.ts +102 -0
  302. package/src/summarization/error-handler.test.ts +127 -0
  303. package/src/summarization/error-handler.ts +111 -0
  304. package/src/summarization/index.ts +102 -0
  305. package/src/summarization/pipeline.test.ts +498 -0
  306. package/src/summarization/pipeline.ts +231 -0
  307. package/src/summarization/prompts.test.ts +269 -0
  308. package/src/summarization/prompts.ts +133 -0
  309. package/src/summarization/provider-factory.test.ts +396 -0
  310. package/src/summarization/provider-factory.ts +178 -0
  311. package/src/summarization/types.ts +184 -0
  312. package/src/summarize/budget-bugs.test.ts +620 -0
  313. package/src/summarize/formatters.ts +419 -0
  314. package/src/summarize/index.ts +20 -0
  315. package/src/summarize/summarizer.test.ts +275 -0
  316. package/src/summarize/summarizer.ts +597 -0
  317. package/src/summarize/verify-bugs.test.ts +238 -0
  318. package/src/types/huggingface-transformers.d.ts +66 -0
  319. package/src/utils/index.ts +1 -0
  320. package/src/utils/tokens.test.ts +142 -0
  321. package/src/utils/tokens.ts +186 -0
  322. package/tests/fixtures/cli/.mdcontext/active-provider.json +7 -0
  323. package/tests/fixtures/cli/.mdcontext/config.json +8 -0
  324. package/tests/fixtures/cli/.mdcontext/embeddings/openai_text-embedding-3-small_512/vectors.bin +0 -0
  325. package/tests/fixtures/cli/.mdcontext/embeddings/openai_text-embedding-3-small_512/vectors.meta.bin +0 -0
  326. package/tests/fixtures/cli/.mdcontext/indexes/documents.json +33 -0
  327. package/tests/fixtures/cli/.mdcontext/indexes/links.json +12 -0
  328. package/tests/fixtures/cli/.mdcontext/indexes/sections.json +247 -0
  329. package/tests/fixtures/cli/README.md +9 -0
  330. package/tests/fixtures/cli/api-reference.md +11 -0
  331. package/tests/fixtures/cli/getting-started.md +11 -0
  332. package/tests/integration/embed-index.test.ts +712 -0
  333. package/tests/integration/search-context.test.ts +469 -0
  334. package/tests/integration/search-semantic.test.ts +522 -0
  335. package/tsconfig.json +26 -0
  336. package/vitest.config.ts +16 -0
  337. package/vitest.setup.ts +12 -0
@@ -0,0 +1,264 @@
1
+ /**
2
+ * HyDE (Hypothetical Document Embeddings) Query Expansion
3
+ *
4
+ * HyDE improves retrieval by generating a hypothetical document that would
5
+ * answer the query, then searching using that document's embedding instead
6
+ * of the raw query embedding.
7
+ *
8
+ * This bridges the semantic gap between short questions and detailed documents,
9
+ * providing 10-30% improvement on ambiguous or complex queries.
10
+ *
11
+ * Paper: "Precise Zero-Shot Dense Retrieval without Relevance Labels"
12
+ * https://arxiv.org/abs/2212.10496
13
+ */
14
+
15
+ import { Effect, Redacted } from 'effect'
16
+ import OpenAI from 'openai'
17
+ import { ApiKeyMissingError, EmbeddingError } from '../errors/index.js'
18
+
19
+ // ============================================================================
20
+ // Types
21
+ // ============================================================================
22
+
23
+ /**
24
+ * Configuration for HyDE query expansion.
25
+ */
26
+ export interface HydeOptions {
27
+ /**
28
+ * OpenAI API key. Can be a plain string or Redacted<string>.
29
+ * Falls back to OPENAI_API_KEY env var if not provided.
30
+ */
31
+ readonly apiKey?: string | Redacted.Redacted<string> | undefined
32
+ /** Model to use for hypothetical document generation. Default: gpt-4o-mini */
33
+ readonly model?: string | undefined
34
+ /** Maximum tokens for the generated document. Default: 256 */
35
+ readonly maxTokens?: number | undefined
36
+ /** Temperature for generation. Lower = more focused. Default: 0.3 */
37
+ readonly temperature?: number | undefined
38
+ /** Custom system prompt for document generation */
39
+ readonly systemPrompt?: string | undefined
40
+ /** Base URL for OpenAI-compatible API (for local models) */
41
+ readonly baseURL?: string | undefined
42
+ }
43
+
44
+ /**
45
+ * Result from HyDE query expansion.
46
+ */
47
+ export interface HydeResult {
48
+ /** The generated hypothetical document */
49
+ readonly hypotheticalDocument: string
50
+ /** The original query for reference */
51
+ readonly originalQuery: string
52
+ /** Model used for generation */
53
+ readonly model: string
54
+ /** Tokens used for generation */
55
+ readonly tokensUsed: number
56
+ /** Estimated cost of the LLM call */
57
+ readonly cost: number
58
+ }
59
+
60
+ // ============================================================================
61
+ // Constants
62
+ // ============================================================================
63
+
64
+ const DEFAULT_MODEL = 'gpt-4o-mini'
65
+ const DEFAULT_MAX_TOKENS = 256
66
+ const DEFAULT_TEMPERATURE = 0.3
67
+
68
+ /**
69
+ * Default system prompt for generating hypothetical documents.
70
+ * Designed to produce concise, factual content that matches documentation style.
71
+ */
72
+ const DEFAULT_SYSTEM_PROMPT = `You are a technical documentation assistant. Given a user's question, write a short, factual passage that would appear in documentation answering this question.
73
+
74
+ Guidelines:
75
+ - Write 2-4 concise paragraphs
76
+ - Use technical but accessible language
77
+ - Include specific details, code examples, or configuration options where relevant
78
+ - Focus on directly answering the question
79
+ - Do not include greetings, preambles, or meta-commentary
80
+ - Write as if this is an excerpt from existing documentation`
81
+
82
+ /**
83
+ * Pricing data for LLM models (per 1M tokens).
84
+ */
85
+ const LLM_PRICING: Record<string, { input: number; output: number }> = {
86
+ 'gpt-4o-mini': { input: 0.15, output: 0.6 },
87
+ 'gpt-4o': { input: 2.5, output: 10 },
88
+ 'gpt-4-turbo': { input: 10, output: 30 },
89
+ 'gpt-3.5-turbo': { input: 0.5, output: 1.5 },
90
+ }
91
+
92
+ // ============================================================================
93
+ // HyDE Implementation
94
+ // ============================================================================
95
+
96
+ /**
97
+ * Generate a hypothetical document that would answer the query.
98
+ *
99
+ * This is the core of HyDE - we ask an LLM to write what documentation
100
+ * answering this query would look like. The resulting text is then
101
+ * embedded and used for similarity search.
102
+ *
103
+ * @param query - The user's search query
104
+ * @param options - HyDE configuration options
105
+ * @returns The generated hypothetical document
106
+ *
107
+ * @throws ApiKeyMissingError - When OPENAI_API_KEY is not set
108
+ * @throws EmbeddingError - When LLM call fails (reusing error type for consistency)
109
+ */
110
+ export const generateHypotheticalDocument = (
111
+ query: string,
112
+ options: HydeOptions = {},
113
+ ): Effect.Effect<HydeResult, ApiKeyMissingError | EmbeddingError> =>
114
+ Effect.gen(function* () {
115
+ // Get API key - resolve from options or environment, normalize to Redacted
116
+ const rawApiKey = options.apiKey ?? process.env.OPENAI_API_KEY
117
+ if (!rawApiKey) {
118
+ return yield* Effect.fail(
119
+ new ApiKeyMissingError({
120
+ provider: 'OpenAI',
121
+ envVar: 'OPENAI_API_KEY',
122
+ }),
123
+ )
124
+ }
125
+
126
+ // Wrap in Redacted if it's a plain string
127
+ const redactedApiKey = Redacted.isRedacted(rawApiKey)
128
+ ? rawApiKey
129
+ : Redacted.make(rawApiKey)
130
+
131
+ const client = new OpenAI({
132
+ apiKey: Redacted.value(redactedApiKey), // Only expose when creating client
133
+ baseURL: options.baseURL,
134
+ })
135
+
136
+ const model = options.model ?? DEFAULT_MODEL
137
+ const maxTokens = options.maxTokens ?? DEFAULT_MAX_TOKENS
138
+ const temperature = options.temperature ?? DEFAULT_TEMPERATURE
139
+ const systemPrompt = options.systemPrompt ?? DEFAULT_SYSTEM_PROMPT
140
+
141
+ // Generate hypothetical document
142
+ const response = yield* Effect.tryPromise({
143
+ try: async () =>
144
+ client.chat.completions.create({
145
+ model,
146
+ messages: [
147
+ { role: 'system', content: systemPrompt },
148
+ { role: 'user', content: query },
149
+ ],
150
+ max_tokens: maxTokens,
151
+ temperature,
152
+ }),
153
+ catch: (error) =>
154
+ new EmbeddingError({
155
+ reason: classifyLLMError(error),
156
+ message: error instanceof Error ? error.message : String(error),
157
+ provider: 'openai',
158
+ cause: error,
159
+ }),
160
+ })
161
+
162
+ const content = response.choices[0]?.message?.content ?? ''
163
+ const inputTokens = response.usage?.prompt_tokens ?? 0
164
+ const outputTokens = response.usage?.completion_tokens ?? 0
165
+ const totalTokens = inputTokens + outputTokens
166
+
167
+ // Calculate cost
168
+ const pricing = LLM_PRICING[model] ?? LLM_PRICING['gpt-4o-mini']!
169
+ const cost =
170
+ (inputTokens / 1_000_000) * pricing.input +
171
+ (outputTokens / 1_000_000) * pricing.output
172
+
173
+ return {
174
+ hypotheticalDocument: content,
175
+ originalQuery: query,
176
+ model,
177
+ tokensUsed: totalTokens,
178
+ cost,
179
+ }
180
+ })
181
+
182
+ /**
183
+ * Classify an LLM error into a known category.
184
+ */
185
+ const classifyLLMError = (
186
+ error: unknown,
187
+ ): 'RateLimit' | 'QuotaExceeded' | 'Network' | 'ModelError' | 'Unknown' => {
188
+ if (error instanceof OpenAI.RateLimitError) {
189
+ return 'RateLimit'
190
+ }
191
+ if (error instanceof OpenAI.BadRequestError) {
192
+ const msg = (error.message || '').toLowerCase()
193
+ if (msg.includes('model')) return 'ModelError'
194
+ }
195
+ if (error instanceof OpenAI.APIConnectionError) {
196
+ return 'Network'
197
+ }
198
+
199
+ if (!(error instanceof Error)) return 'Unknown'
200
+ const msg = error.message.toLowerCase()
201
+
202
+ if (msg.includes('429') || msg.includes('rate limit')) return 'RateLimit'
203
+ if (msg.includes('quota') || msg.includes('billing')) return 'QuotaExceeded'
204
+ if (msg.includes('econnrefused') || msg.includes('network')) return 'Network'
205
+ if (msg.includes('model') && msg.includes('not found')) return 'ModelError'
206
+
207
+ return 'Unknown'
208
+ }
209
+
210
+ /**
211
+ * Check if HyDE is available (API key is set).
212
+ *
213
+ * @returns true if HyDE can be used
214
+ */
215
+ export const isHydeAvailable = (): boolean => {
216
+ return Boolean(process.env.OPENAI_API_KEY)
217
+ }
218
+
219
+ /**
220
+ * Detect if a query would benefit from HyDE expansion.
221
+ *
222
+ * HyDE works best for:
223
+ * - Questions (who, what, where, when, why, how)
224
+ * - Complex or ambiguous queries
225
+ * - Queries seeking procedural information
226
+ *
227
+ * HyDE works poorly for:
228
+ * - Single-word queries
229
+ * - Exact phrase searches
230
+ * - Very short queries (< 3 words)
231
+ *
232
+ * @param query - The search query
233
+ * @returns true if HyDE would likely help
234
+ */
235
+ export const shouldUseHyde = (query: string): boolean => {
236
+ const normalizedQuery = query.toLowerCase().trim()
237
+ const words = normalizedQuery.split(/\s+/)
238
+
239
+ // Skip very short queries
240
+ if (words.length < 3) return false
241
+
242
+ // Skip if it looks like an exact phrase search
243
+ if (query.startsWith('"') && query.endsWith('"')) return false
244
+
245
+ // Questions are good candidates
246
+ const questionPatterns = [
247
+ /^(how|what|why|when|where|who|which)\s/i,
248
+ /^(can|could|should|would|is|are|does|do)\s/i,
249
+ /\?$/,
250
+ ]
251
+ if (questionPatterns.some((p) => p.test(normalizedQuery))) return true
252
+
253
+ // Procedural queries are good candidates
254
+ const proceduralPatterns = [
255
+ /\b(setup|install|configure|implement|create|build|fix|debug|resolve)\b/i,
256
+ /\b(step|guide|tutorial|example|documentation)\b/i,
257
+ ]
258
+ if (proceduralPatterns.some((p) => p.test(normalizedQuery))) return true
259
+
260
+ // Longer queries (6+ words) often benefit
261
+ if (words.length >= 6) return true
262
+
263
+ return false
264
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Embeddings module exports
3
+ */
4
+
5
+ export * from './hyde.js'
6
+ export * from './openai-provider.js'
7
+ export * from './semantic-search.js'
8
+ export * from './types.js'
9
+ export * from './vector-store.js'
10
+ export * from './voyage-provider.js'
@@ -0,0 +1,414 @@
1
+ /**
2
+ * OpenAI embedding provider
3
+ */
4
+
5
+ import { Effect, Redacted } from 'effect'
6
+ import OpenAI from 'openai'
7
+ import {
8
+ ApiKeyInvalidError,
9
+ ApiKeyMissingError,
10
+ EmbeddingError,
11
+ } from '../errors/index.js'
12
+ import pricingData from './pricing.json' with { type: 'json' }
13
+ import {
14
+ getRecommendedDimensions,
15
+ inferProviderFromUrl,
16
+ supportsMatryoshka,
17
+ validateModelDimensions,
18
+ } from './provider-constants.js'
19
+ import type {
20
+ EmbeddingProvider,
21
+ EmbeddingResult,
22
+ EmbedOptions,
23
+ } from './types.js'
24
+
25
+ // ============================================================================
26
+ // Cost Constants
27
+ // ============================================================================
28
+
29
+ /**
30
+ * OpenAI embedding model pricing data.
31
+ *
32
+ * Prices per 1M tokens. Loaded from pricing.json for easy updates.
33
+ *
34
+ * Maintenance: Update src/embeddings/pricing.json quarterly from
35
+ * https://platform.openai.com/docs/pricing
36
+ */
37
+ export const PRICING_DATA = {
38
+ /** Last update date in YYYY-MM format */
39
+ lastUpdated: pricingData.lastUpdated,
40
+ /** Source URL for verification */
41
+ source: pricingData.sources.openai,
42
+ /** Prices per 1M tokens by model */
43
+ prices: pricingData.openai as Record<string, number>,
44
+ }
45
+
46
+ /**
47
+ * Check if pricing data is stale (>90 days old).
48
+ *
49
+ * @returns Warning message if stale, null otherwise
50
+ */
51
+ export const checkPricingFreshness = (): string | null => {
52
+ const [year, month] = PRICING_DATA.lastUpdated.split('-').map(Number)
53
+ if (!year || !month) return null
54
+
55
+ const lastUpdated = new Date(year, month - 1, 1) // Month is 0-indexed
56
+ const now = new Date()
57
+ const daysSince = Math.floor(
58
+ (now.getTime() - lastUpdated.getTime()) / (1000 * 60 * 60 * 24),
59
+ )
60
+
61
+ if (daysSince > 90) {
62
+ return `Pricing data is ${daysSince} days old. May not reflect current rates.`
63
+ }
64
+ return null
65
+ }
66
+
67
+ /**
68
+ * Get the pricing date for display.
69
+ *
70
+ * @returns Formatted string like "2024-09"
71
+ */
72
+ export const getPricingDate = (): string => PRICING_DATA.lastUpdated
73
+
74
+ // ============================================================================
75
+ // OpenAI Provider
76
+ // ============================================================================
77
+
78
+ export interface OpenAIProviderOptions {
79
+ /**
80
+ * API key for the provider. Can be a plain string or a Redacted<string>.
81
+ * If not provided, falls back to environment variables:
82
+ * - OPENROUTER_API_KEY (if using OpenRouter)
83
+ * - OPENAI_API_KEY (default)
84
+ */
85
+ readonly apiKey?: string | Redacted.Redacted<string> | undefined
86
+ readonly model?: string | undefined
87
+ readonly batchSize?: number | undefined
88
+ readonly baseURL?: string | undefined
89
+ /**
90
+ * Number of embedding dimensions. If not specified, uses recommended
91
+ * dimensions for the model (512 for Matryoshka models, native for others).
92
+ */
93
+ readonly dimensions?: number | undefined
94
+ /**
95
+ * Provider name for error context (e.g., 'ollama', 'lm-studio')
96
+ * Defaults to 'openai' if baseURL is not set
97
+ */
98
+ readonly providerName?: string | undefined
99
+ /**
100
+ * Request timeout in milliseconds.
101
+ * Default: 30000 (30 seconds)
102
+ */
103
+ readonly timeout?: number | undefined
104
+ }
105
+
106
+ export class OpenAIProvider implements EmbeddingProvider {
107
+ readonly name: string
108
+ readonly dimensions: number
109
+ /** Provider name for error context */
110
+ readonly providerName: string
111
+ /** Model name */
112
+ readonly model: string
113
+ /** Base URL for API requests */
114
+ readonly baseURL: string | undefined
115
+
116
+ private readonly client: OpenAI
117
+ private readonly batchSize: number
118
+
119
+ private constructor(
120
+ apiKey: Redacted.Redacted<string>,
121
+ options: OpenAIProviderOptions = {},
122
+ ) {
123
+ this.baseURL = options.baseURL
124
+ this.client = new OpenAI({
125
+ apiKey: Redacted.value(apiKey),
126
+ baseURL: options.baseURL,
127
+ timeout: options.timeout ?? 30000,
128
+ maxRetries: 2,
129
+ })
130
+ this.model = options.model ?? 'text-embedding-3-small'
131
+ this.batchSize = options.batchSize ?? 100
132
+ this.providerName =
133
+ options.providerName ?? this.inferProviderName(options.baseURL)
134
+ this.name = `${this.providerName}:${this.model}`
135
+
136
+ const recommendedDims = getRecommendedDimensions(this.model)
137
+ this.dimensions = options.dimensions ?? recommendedDims ?? 512
138
+ }
139
+
140
+ /**
141
+ * Infer the provider name from the baseURL.
142
+ * Delegates to centralized inferProviderFromUrl for single source of truth.
143
+ */
144
+ private inferProviderName(baseURL: string | undefined): string {
145
+ return inferProviderFromUrl(baseURL)
146
+ }
147
+
148
+ /**
149
+ * Create an OpenAI provider instance.
150
+ * Returns an Effect that fails with ApiKeyMissingError if no API key is available.
151
+ *
152
+ * API keys are handled securely using Effect's Redacted type to prevent
153
+ * accidental logging of sensitive values.
154
+ */
155
+ static create(
156
+ options: OpenAIProviderOptions = {},
157
+ ): Effect.Effect<OpenAIProvider, ApiKeyMissingError> {
158
+ // For OpenRouter provider, check OPENROUTER_API_KEY first, then fall back to OPENAI_API_KEY
159
+ const isOpenRouter =
160
+ options.baseURL?.includes('openrouter') ||
161
+ options.providerName === 'openrouter'
162
+
163
+ // Normalize API key to Redacted<string>
164
+ // If apiKey is already Redacted, use it; if string, wrap it; if undefined, check env vars
165
+ const resolveApiKey = ():
166
+ | Redacted.Redacted<string>
167
+ | string
168
+ | undefined => {
169
+ if (options.apiKey !== undefined) {
170
+ return options.apiKey
171
+ }
172
+ return (
173
+ (isOpenRouter ? process.env.OPENROUTER_API_KEY : undefined) ??
174
+ process.env.OPENAI_API_KEY
175
+ )
176
+ }
177
+
178
+ const rawApiKey = resolveApiKey()
179
+ if (!rawApiKey) {
180
+ return Effect.fail(
181
+ new ApiKeyMissingError({
182
+ provider: isOpenRouter ? 'OpenRouter' : 'OpenAI',
183
+ envVar: isOpenRouter ? 'OPENROUTER_API_KEY' : 'OPENAI_API_KEY',
184
+ }),
185
+ )
186
+ }
187
+
188
+ // Wrap in Redacted if it's a plain string
189
+ const redactedApiKey = Redacted.isRedacted(rawApiKey)
190
+ ? rawApiKey
191
+ : Redacted.make(rawApiKey)
192
+
193
+ // Check key format for warnings (need to access value temporarily)
194
+ const apiKeyValue = Redacted.value(redactedApiKey)
195
+ const shouldWarnOpenRouter =
196
+ isOpenRouter &&
197
+ apiKeyValue.startsWith('sk-') &&
198
+ !apiKeyValue.startsWith('sk-or-')
199
+
200
+ // Validate dimensions if explicitly set
201
+ const model = options.model ?? 'text-embedding-3-small'
202
+ const dimensionValidation = options.dimensions
203
+ ? validateModelDimensions(model, options.dimensions)
204
+ : { isValid: true }
205
+
206
+ return Effect.succeed(new OpenAIProvider(redactedApiKey, options)).pipe(
207
+ shouldWarnOpenRouter
208
+ ? Effect.tap(() =>
209
+ Effect.logWarning(
210
+ '⚠️ Using OpenAI key format with OpenRouter. Consider setting OPENROUTER_API_KEY with a key starting with "sk-or-"',
211
+ ),
212
+ )
213
+ : (self) => self,
214
+ // Warn about invalid dimension configuration
215
+ dimensionValidation.warning
216
+ ? Effect.tap(() =>
217
+ Effect.logWarning(`⚠️ ${dimensionValidation.warning}`),
218
+ )
219
+ : (self) => self,
220
+ )
221
+ }
222
+
223
+ async embed(
224
+ texts: string[],
225
+ options?: EmbedOptions,
226
+ ): Promise<EmbeddingResult> {
227
+ if (texts.length === 0) {
228
+ return { embeddings: [], tokensUsed: 0, cost: 0 }
229
+ }
230
+
231
+ const allEmbeddings: number[][] = []
232
+ let totalTokens = 0
233
+ const totalBatches = Math.ceil(texts.length / this.batchSize)
234
+
235
+ try {
236
+ // Process in batches
237
+ for (let i = 0; i < texts.length; i += this.batchSize) {
238
+ const batch = texts.slice(i, i + this.batchSize)
239
+ const batchIndex = Math.floor(i / this.batchSize)
240
+
241
+ // Only pass dimensions parameter for models that support it (Matryoshka)
242
+ // Non-Matryoshka models will use their native dimensions automatically
243
+ const embedParams: OpenAI.Embeddings.EmbeddingCreateParams = {
244
+ model: this.model,
245
+ input: batch,
246
+ }
247
+
248
+ // Only add dimensions parameter for Matryoshka-compatible models
249
+ if (supportsMatryoshka(this.model)) {
250
+ embedParams.dimensions = this.dimensions
251
+ }
252
+
253
+ const response = await this.client.embeddings.create(embedParams)
254
+
255
+ for (const item of response.data) {
256
+ allEmbeddings.push(item.embedding)
257
+ }
258
+
259
+ totalTokens += response.usage?.total_tokens ?? 0
260
+
261
+ // Report batch progress
262
+ if (options?.onBatchProgress) {
263
+ options.onBatchProgress({
264
+ batchIndex: batchIndex + 1,
265
+ totalBatches,
266
+ processedTexts: Math.min(i + this.batchSize, texts.length),
267
+ totalTexts: texts.length,
268
+ })
269
+ }
270
+ }
271
+ } catch (error) {
272
+ // Check for authentication errors (401 Unauthorized, invalid API key)
273
+ if (error instanceof OpenAI.AuthenticationError) {
274
+ throw new ApiKeyInvalidError({
275
+ provider: this.providerName,
276
+ details: error.message,
277
+ })
278
+ }
279
+ // Wrap error with provider context for better error messages
280
+ throw new EmbeddingError({
281
+ reason: this.classifyError(error),
282
+ message: error instanceof Error ? error.message : String(error),
283
+ provider: this.providerName,
284
+ cause: error,
285
+ })
286
+ }
287
+
288
+ // Calculate cost (only for paid providers)
289
+ const pricePerMillion =
290
+ this.providerName === 'openai' || this.providerName === 'openrouter'
291
+ ? (PRICING_DATA.prices[this.model] ?? 0.02)
292
+ : 0 // Local providers are free
293
+ const cost = (totalTokens / 1_000_000) * pricePerMillion
294
+
295
+ return {
296
+ embeddings: allEmbeddings,
297
+ tokensUsed: totalTokens,
298
+ cost,
299
+ }
300
+ }
301
+
302
+ /**
303
+ * Classify an error into a known category for better error handling.
304
+ * Uses OpenAI SDK error types where available, falls back to string matching
305
+ * for non-OpenAI providers (Ollama, LM Studio, OpenRouter).
306
+ */
307
+ private classifyError(
308
+ error: unknown,
309
+ ): 'RateLimit' | 'QuotaExceeded' | 'Network' | 'ModelError' | 'Unknown' {
310
+ // Use OpenAI SDK error types when available
311
+ if (error instanceof OpenAI.RateLimitError) {
312
+ return 'RateLimit'
313
+ }
314
+ if (error instanceof OpenAI.BadRequestError) {
315
+ const msg = error.message.toLowerCase()
316
+ if (msg.includes('model')) return 'ModelError'
317
+ }
318
+ if (error instanceof OpenAI.APIConnectionError) {
319
+ return 'Network'
320
+ }
321
+
322
+ // Fallback to string matching for non-SDK errors (local providers, etc.)
323
+ if (!(error instanceof Error)) return 'Unknown'
324
+ const msg = error.message.toLowerCase()
325
+
326
+ // Rate limiting
327
+ if (
328
+ msg.includes('429') ||
329
+ msg.includes('rate limit') ||
330
+ msg.includes('too many requests')
331
+ ) {
332
+ return 'RateLimit'
333
+ }
334
+
335
+ // Quota/billing issues
336
+ if (
337
+ msg.includes('quota') ||
338
+ msg.includes('insufficient') ||
339
+ msg.includes('billing')
340
+ ) {
341
+ return 'QuotaExceeded'
342
+ }
343
+
344
+ // Network issues
345
+ if (
346
+ msg.includes('econnrefused') ||
347
+ msg.includes('timeout') ||
348
+ msg.includes('network') ||
349
+ msg.includes('enotfound') ||
350
+ msg.includes('connection')
351
+ ) {
352
+ return 'Network'
353
+ }
354
+
355
+ // Model issues
356
+ if (
357
+ msg.includes('model') &&
358
+ (msg.includes('not found') ||
359
+ msg.includes('not exist') ||
360
+ msg.includes('invalid'))
361
+ ) {
362
+ return 'ModelError'
363
+ }
364
+
365
+ return 'Unknown'
366
+ }
367
+ }
368
+
369
+ // ============================================================================
370
+ // Factory Functions
371
+ // ============================================================================
372
+
373
+ /**
374
+ * Create an OpenAI embedding provider.
375
+ * Returns an Effect that fails with ApiKeyMissingError if no API key is available.
376
+ *
377
+ * Usage:
378
+ * ```typescript
379
+ * const provider = yield* createOpenAIProvider()
380
+ * const result = yield* Effect.tryPromise(() => provider.embed(texts))
381
+ * ```
382
+ */
383
+ export const createOpenAIProvider = (
384
+ options?: OpenAIProviderOptions,
385
+ ): Effect.Effect<EmbeddingProvider, ApiKeyMissingError> =>
386
+ OpenAIProvider.create(options)
387
+
388
+ /**
389
+ * Wrap an embedding operation to catch InvalidApiKeyError thrown during embed().
390
+ * Use this when calling provider.embed() to convert thrown errors to Effect failures.
391
+ *
392
+ * Usage:
393
+ * ```typescript
394
+ * const result = yield* wrapEmbedding(provider.embed(texts))
395
+ * ```
396
+ */
397
+ export const wrapEmbedding = (
398
+ embedPromise: Promise<EmbeddingResult>,
399
+ providerName = 'openai',
400
+ ): Effect.Effect<EmbeddingResult, ApiKeyInvalidError | EmbeddingError> =>
401
+ Effect.tryPromise({
402
+ try: () => embedPromise,
403
+ catch: (e) => {
404
+ if (e instanceof ApiKeyInvalidError) {
405
+ return e
406
+ }
407
+ return new EmbeddingError({
408
+ reason: 'Unknown',
409
+ message: e instanceof Error ? e.message : String(e),
410
+ provider: providerName,
411
+ cause: e,
412
+ })
413
+ },
414
+ })
@@ -0,0 +1,22 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "description": "Embedding model pricing data. Update quarterly from provider pricing pages.",
4
+ "lastUpdated": "2026-01",
5
+ "sources": {
6
+ "openai": "https://platform.openai.com/docs/pricing",
7
+ "voyage": "https://docs.voyageai.com/docs/pricing"
8
+ },
9
+ "openai": {
10
+ "text-embedding-3-small": 0.02,
11
+ "text-embedding-3-large": 0.13,
12
+ "text-embedding-ada-002": 0.1
13
+ },
14
+ "voyage": {
15
+ "voyage-3.5-lite": 0.02,
16
+ "voyage-3": 0.06,
17
+ "voyage-code-3": 0.18,
18
+ "voyage-2": 0.1,
19
+ "voyage-large-2": 0.12,
20
+ "voyage-code-2": 0.12
21
+ }
22
+ }