@far-world-labs/verblets 0.1.1 → 0.1.3

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 (300) hide show
  1. package/.cursor/launch.json +30 -0
  2. package/.cursor/settings.json +20 -0
  3. package/.github/workflows/branch-protection.yml +22 -0
  4. package/.github/workflows/ci.yml +120 -0
  5. package/.prettierrc +6 -0
  6. package/.release-it.json +4 -1
  7. package/.vscode/launch.json +31 -0
  8. package/AGENTS.md +220 -0
  9. package/DEVELOPING.md +105 -0
  10. package/README.md +254 -0
  11. package/eslint.config.js +80 -0
  12. package/package.json +29 -17
  13. package/scripts/generate-test/index.js +29 -3
  14. package/scripts/runner/index.js +26 -0
  15. package/scripts/simple-editor/index.js +29 -18
  16. package/scripts/summarize-files/index.js +28 -4
  17. package/src/chains/README.md +30 -0
  18. package/src/chains/anonymize/README.md +21 -0
  19. package/src/chains/anonymize/index.examples.js +75 -0
  20. package/src/chains/anonymize/index.js +121 -0
  21. package/src/chains/anonymize/index.spec.js +78 -0
  22. package/src/chains/bulk-central-tendency/index.examples.js +138 -0
  23. package/src/chains/bulk-central-tendency/index.js +91 -0
  24. package/src/chains/bulk-filter/README.md +21 -0
  25. package/src/chains/bulk-filter/index.examples.js +22 -0
  26. package/src/chains/bulk-filter/index.js +58 -0
  27. package/src/chains/bulk-filter/index.spec.js +38 -0
  28. package/src/chains/bulk-find/README.md +16 -0
  29. package/src/chains/bulk-find/index.examples.js +20 -0
  30. package/src/chains/bulk-find/index.js +30 -0
  31. package/src/chains/bulk-find/index.spec.js +26 -0
  32. package/src/chains/bulk-group/README.md +23 -0
  33. package/src/chains/bulk-group/index.examples.js +18 -0
  34. package/src/chains/bulk-group/index.js +34 -0
  35. package/src/chains/bulk-group/index.spec.js +41 -0
  36. package/src/chains/bulk-map/README.md +43 -0
  37. package/src/chains/bulk-map/index.examples.js +17 -0
  38. package/src/chains/bulk-map/index.js +86 -0
  39. package/src/chains/bulk-map/index.spec.js +44 -0
  40. package/src/chains/bulk-reduce/README.md +12 -0
  41. package/src/chains/bulk-reduce/index.examples.js +15 -0
  42. package/src/chains/bulk-reduce/index.js +13 -0
  43. package/src/chains/bulk-reduce/index.spec.js +25 -0
  44. package/src/chains/bulk-score/README.md +16 -0
  45. package/src/chains/bulk-score/bulk-score-result.json +18 -0
  46. package/src/chains/bulk-score/index.examples.js +22 -0
  47. package/src/chains/bulk-score/index.js +133 -0
  48. package/src/chains/bulk-score/index.spec.js +30 -0
  49. package/src/chains/category-samples/README.md +61 -0
  50. package/src/chains/category-samples/index.examples.js +103 -0
  51. package/src/chains/category-samples/index.js +134 -0
  52. package/src/chains/collect-terms/README.md +12 -0
  53. package/src/chains/collect-terms/index.examples.js +16 -0
  54. package/src/chains/collect-terms/index.js +44 -0
  55. package/src/chains/collect-terms/index.spec.js +25 -0
  56. package/src/chains/date/README.md +12 -0
  57. package/src/chains/date/index.examples.js +47 -0
  58. package/src/chains/date/index.js +74 -0
  59. package/src/chains/date/index.spec.js +62 -0
  60. package/src/chains/disambiguate/README.md +22 -0
  61. package/src/chains/disambiguate/disambiguate-meanings-result.json +16 -0
  62. package/src/chains/disambiguate/index.examples.js +18 -0
  63. package/src/chains/disambiguate/index.js +92 -0
  64. package/src/chains/disambiguate/index.spec.js +25 -0
  65. package/src/chains/dismantle/README.md +67 -0
  66. package/src/chains/dismantle/dismantle.examples.js +27 -0
  67. package/src/chains/dismantle/index.js +6 -17
  68. package/src/chains/dismantle/index.spec.js +1 -2
  69. package/src/chains/expect/README.md +171 -0
  70. package/src/chains/expect/index.examples.js +146 -0
  71. package/src/chains/expect/index.js +173 -0
  72. package/src/chains/expect/index.spec.js +324 -0
  73. package/src/chains/filter-ambiguous/README.md +11 -0
  74. package/src/chains/filter-ambiguous/index.examples.js +20 -0
  75. package/src/chains/filter-ambiguous/index.js +49 -0
  76. package/src/chains/filter-ambiguous/index.spec.js +31 -0
  77. package/src/chains/glossary/README.md +19 -0
  78. package/src/chains/glossary/index.examples.js +386 -0
  79. package/src/chains/glossary/index.js +75 -0
  80. package/src/chains/glossary/index.spec.js +19 -0
  81. package/src/chains/intersections/README.md +152 -0
  82. package/src/chains/intersections/index.examples.js +279 -0
  83. package/src/chains/intersections/index.js +366 -0
  84. package/src/chains/intersections/intersection-result.json +38 -0
  85. package/src/chains/list/index.examples.js +12 -16
  86. package/src/chains/list/index.js +106 -53
  87. package/src/chains/list/index.spec.js +8 -9
  88. package/src/chains/list/list-result.json +16 -0
  89. package/src/chains/llm-logger/README.md +208 -0
  90. package/src/chains/llm-logger/index.js +205 -0
  91. package/src/chains/llm-logger/index.spec.js +330 -0
  92. package/src/chains/questions/index.examples.js +2 -1
  93. package/src/chains/questions/index.js +14 -15
  94. package/src/chains/scan-js/index.js +6 -9
  95. package/src/chains/set-interval/README.md +81 -0
  96. package/src/chains/set-interval/index.examples.js +36 -0
  97. package/src/chains/set-interval/index.js +131 -0
  98. package/src/chains/set-interval/index.spec.js +70 -0
  99. package/src/chains/socratic/README.md +17 -0
  100. package/src/chains/socratic/index.js +64 -0
  101. package/src/chains/socratic/index.spec.js +24 -0
  102. package/src/chains/sort/index.examples.js +3 -7
  103. package/src/chains/sort/index.js +65 -15
  104. package/src/chains/sort/index.spec.js +5 -8
  105. package/src/chains/sort/sort-result.json +16 -0
  106. package/src/chains/summary-map/README.md +9 -1
  107. package/src/chains/summary-map/index.examples.js +9 -2
  108. package/src/chains/summary-map/index.js +43 -25
  109. package/src/chains/summary-map/index.spec.js +78 -3
  110. package/src/chains/test/index.js +9 -13
  111. package/src/chains/test-advice/index.js +4 -5
  112. package/src/chains/themes/README.md +20 -0
  113. package/src/chains/themes/index.examples.js +17 -0
  114. package/src/chains/themes/index.js +28 -0
  115. package/src/chains/themes/index.spec.js +19 -0
  116. package/src/chains/veiled-variants/index.examples.js +18 -0
  117. package/src/chains/veiled-variants/index.js +107 -0
  118. package/src/chains/veiled-variants/index.spec.js +40 -0
  119. package/src/constants/common.js +0 -2
  120. package/src/constants/models.js +172 -0
  121. package/src/index.js +178 -18
  122. package/src/json-schemas/README.md +13 -0
  123. package/src/json-schemas/index.js +8 -14
  124. package/src/json-schemas/schema-dot-org-photograph.json +11 -5
  125. package/src/json-schemas/schema-dot-org-place.json +78 -5
  126. package/src/lib/README.md +26 -0
  127. package/src/lib/bulk-filter/README.md +22 -0
  128. package/src/lib/bulk-filter/index.examples.js +27 -0
  129. package/src/lib/bulk-filter/index.js +63 -0
  130. package/src/lib/bulk-filter/index.spec.js +38 -0
  131. package/src/lib/bulk-find/README.md +18 -0
  132. package/src/lib/bulk-find/index.examples.js +19 -0
  133. package/src/lib/bulk-find/index.js +30 -0
  134. package/src/lib/bulk-find/index.spec.js +41 -0
  135. package/src/lib/chatgpt/index.js +63 -43
  136. package/src/lib/combinations/index.js +30 -0
  137. package/src/lib/combinations/index.spec.js +23 -0
  138. package/src/lib/functional/index.js +28 -0
  139. package/src/lib/logger-service/index.js +32 -0
  140. package/src/lib/parse-js-parts/index.js +9 -21
  141. package/src/lib/parse-llm-list/README.md +39 -0
  142. package/src/lib/parse-llm-list/index.js +54 -0
  143. package/src/lib/parse-llm-list/index.spec.js +59 -0
  144. package/src/lib/path-aliases/index.js +1 -3
  145. package/src/lib/path-aliases/index.spec.js +2 -8
  146. package/src/lib/pave/index.js +4 -4
  147. package/src/lib/pave/index.spec.js +6 -3
  148. package/src/lib/prompt-cache/index.js +14 -10
  149. package/src/lib/retry/index.js +11 -8
  150. package/src/lib/ring-buffer/README.md +460 -0
  151. package/src/lib/ring-buffer/index.js +1074 -0
  152. package/src/lib/search-best-first/city-walk.spec.js +37 -0
  153. package/src/lib/search-best-first/index.js +42 -11
  154. package/src/lib/search-best-first/index.spec.js +35 -0
  155. package/src/lib/search-js-files/index.js +44 -47
  156. package/src/lib/search-js-files/scan-file.js +10 -21
  157. package/src/lib/shorten-text/index.js +2 -7
  158. package/src/lib/shorten-text/index.spec.js +3 -3
  159. package/src/lib/strip-response/index.js +2 -7
  160. package/src/lib/template-replace/index.js +23 -0
  161. package/src/lib/template-replace/index.spec.js +60 -0
  162. package/src/lib/to-date/index.js +11 -0
  163. package/src/lib/to-number/index.js +1 -1
  164. package/src/lib/transcribe/index.js +26 -9
  165. package/src/prompts/README.md +3 -1
  166. package/src/prompts/as-object-with-schema.js +3 -8
  167. package/src/prompts/as-schema-org-text.js +10 -2
  168. package/src/prompts/code-features.js +1 -5
  169. package/src/prompts/constants.js +27 -27
  170. package/src/prompts/generate-collection.js +1 -1
  171. package/src/prompts/intent.js +16 -22
  172. package/src/prompts/select-from-threshold.js +1 -2
  173. package/src/prompts/sort.js +4 -8
  174. package/src/prompts/style.js +4 -7
  175. package/src/prompts/wrap-list.js +1 -4
  176. package/src/services/llm-model/global-overrides.spec.js +432 -0
  177. package/src/services/llm-model/index.js +234 -40
  178. package/src/services/llm-model/model.js +2 -2
  179. package/src/services/llm-model/negotiate.spec.js +447 -0
  180. package/src/services/redis/index.js +70 -7
  181. package/src/test/setup.js +20 -0
  182. package/src/verblets/README.md +26 -0
  183. package/src/verblets/auto/index.examples.js +12 -9
  184. package/src/verblets/auto/index.js +10 -10
  185. package/src/verblets/auto/index.spec.js +4 -6
  186. package/src/verblets/bool/README.md +36 -0
  187. package/src/verblets/bool/index.examples.js +53 -1
  188. package/src/verblets/bool/index.js +6 -9
  189. package/src/verblets/bool/index.spec.js +1 -3
  190. package/src/verblets/central-tendency/README.md +166 -0
  191. package/src/verblets/central-tendency/central-tendency-result.json +24 -0
  192. package/src/verblets/central-tendency/index.examples.js +196 -0
  193. package/src/verblets/central-tendency/index.js +171 -0
  194. package/src/verblets/central-tendency/index.spec.js +148 -0
  195. package/src/verblets/enum/index.examples.js +1 -4
  196. package/src/verblets/enum/index.js +7 -4
  197. package/src/verblets/expect/README.md +64 -0
  198. package/src/verblets/expect/index.examples.js +109 -0
  199. package/src/verblets/expect/index.js +75 -0
  200. package/src/verblets/expect/index.spec.js +127 -0
  201. package/src/verblets/intent/index.examples.js +95 -7
  202. package/src/verblets/intent/index.js +56 -68
  203. package/src/verblets/intersection/README.md +16 -0
  204. package/src/verblets/intersection/index.examples.js +89 -0
  205. package/src/verblets/intersection/index.js +84 -0
  206. package/src/verblets/intersection/index.spec.js +60 -0
  207. package/src/verblets/intersection/intersection-result.json +16 -0
  208. package/src/verblets/list-expand/README.md +10 -0
  209. package/src/verblets/list-expand/index.examples.js +14 -0
  210. package/src/verblets/list-expand/index.js +104 -0
  211. package/src/verblets/list-expand/index.spec.js +18 -0
  212. package/src/verblets/list-expand/list-expand-result.json +16 -0
  213. package/src/verblets/list-filter/README.md +22 -0
  214. package/src/verblets/list-filter/index.examples.js +26 -0
  215. package/src/verblets/list-filter/index.js +18 -0
  216. package/src/verblets/list-filter/index.spec.js +19 -0
  217. package/src/verblets/list-find/README.md +11 -0
  218. package/src/verblets/list-find/index.examples.js +15 -0
  219. package/src/verblets/list-find/index.js +17 -0
  220. package/src/verblets/list-find/index.spec.js +19 -0
  221. package/src/verblets/list-group/README.md +16 -0
  222. package/src/verblets/list-group/index.examples.js +16 -0
  223. package/src/verblets/list-group/index.js +112 -0
  224. package/src/verblets/list-group/index.spec.js +35 -0
  225. package/src/verblets/list-group/list-group-result.json +16 -0
  226. package/src/verblets/list-map/README.md +11 -0
  227. package/src/verblets/list-map/index.examples.js +15 -0
  228. package/src/verblets/list-map/index.js +26 -0
  229. package/src/verblets/list-map/index.spec.js +17 -0
  230. package/src/verblets/list-reduce/README.md +10 -0
  231. package/src/verblets/list-reduce/index.examples.js +14 -0
  232. package/src/verblets/list-reduce/index.js +21 -0
  233. package/src/verblets/list-reduce/index.spec.js +27 -0
  234. package/src/verblets/list-reduce/index.spec.jsx +27 -0
  235. package/src/verblets/name/README.md +15 -0
  236. package/src/verblets/name/index.examples.js +28 -0
  237. package/src/verblets/name/index.js +19 -0
  238. package/src/verblets/name/index.spec.js +33 -0
  239. package/src/verblets/name-similar-to/README.md +26 -0
  240. package/src/verblets/name-similar-to/index.examples.js +18 -0
  241. package/src/verblets/name-similar-to/index.js +20 -0
  242. package/src/verblets/name-similar-to/index.spec.js +13 -0
  243. package/src/verblets/number/index.examples.js +173 -7
  244. package/src/verblets/number/index.js +5 -2
  245. package/src/verblets/number/index.spec.js +1 -3
  246. package/src/verblets/number-with-units/index.examples.js +5 -1
  247. package/src/verblets/number-with-units/index.js +74 -9
  248. package/src/verblets/number-with-units/number-with-units-result.json +23 -0
  249. package/src/verblets/schema-org/index.examples.js +2 -7
  250. package/src/verblets/schema-org/index.js +32 -3
  251. package/src/verblets/sentiment/README.md +10 -0
  252. package/src/verblets/sentiment/index.examples.js +20 -0
  253. package/src/verblets/sentiment/index.js +9 -0
  254. package/src/verblets/sentiment/index.spec.js +20 -0
  255. package/src/verblets/to-object/index.js +10 -15
  256. package/src/verblets/to-object/index.spec.js +1 -4
  257. package/.eslintrc.json +0 -42
  258. package/docs/README.md +0 -41
  259. package/docs/babel.config.js +0 -3
  260. package/docs/blog/2019-05-28-first-blog-post.md +0 -12
  261. package/docs/blog/2019-05-29-long-blog-post.md +0 -44
  262. package/docs/blog/2021-08-01-mdx-blog-post.mdx +0 -20
  263. package/docs/blog/2021-08-26-welcome/docusaurus-plushie-banner.jpeg +0 -0
  264. package/docs/blog/2021-08-26-welcome/index.md +0 -25
  265. package/docs/blog/authors.yml +0 -17
  266. package/docs/docs/api/bool.md +0 -74
  267. package/docs/docs/api/search.md +0 -51
  268. package/docs/docs/intro.md +0 -47
  269. package/docs/docs/tutorial-basics/_category_.json +0 -8
  270. package/docs/docs/tutorial-basics/congratulations.md +0 -23
  271. package/docs/docs/tutorial-basics/create-a-blog-post.md +0 -34
  272. package/docs/docs/tutorial-basics/create-a-document.md +0 -57
  273. package/docs/docs/tutorial-basics/create-a-page.md +0 -43
  274. package/docs/docs/tutorial-basics/deploy-your-site.md +0 -31
  275. package/docs/docs/tutorial-basics/markdown-features.mdx +0 -152
  276. package/docs/docs/tutorial-extras/_category_.json +0 -7
  277. package/docs/docs/tutorial-extras/img/docsVersionDropdown.png +0 -0
  278. package/docs/docs/tutorial-extras/img/localeDropdown.png +0 -0
  279. package/docs/docs/tutorial-extras/manage-docs-versions.md +0 -55
  280. package/docs/docs/tutorial-extras/translate-your-site.md +0 -88
  281. package/docs/docusaurus.config.js +0 -120
  282. package/docs/package.json +0 -44
  283. package/docs/sidebars.js +0 -31
  284. package/docs/src/components/HomepageFeatures/index.js +0 -61
  285. package/docs/src/components/HomepageFeatures/styles.module.css +0 -11
  286. package/docs/src/css/custom.css +0 -30
  287. package/docs/src/pages/index.js +0 -43
  288. package/docs/src/pages/index.module.css +0 -23
  289. package/docs/src/pages/markdown-page.md +0 -7
  290. package/docs/static/.nojekyll +0 -0
  291. package/docs/static/img/docusaurus-social-card.jpg +0 -0
  292. package/docs/static/img/docusaurus.png +0 -0
  293. package/docs/static/img/favicon.ico +0 -0
  294. package/docs/static/img/logo.svg +0 -1
  295. package/docs/static/img/undraw_docusaurus_mountain.svg +0 -171
  296. package/docs/static/img/undraw_docusaurus_react.svg +0 -170
  297. package/docs/static/img/undraw_docusaurus_tree.svg +0 -40
  298. package/src/constants/openai.js +0 -65
  299. /package/{.vite.config.examples.js → .vitest.config.examples.js} +0 -0
  300. /package/{.vite.config.js → .vitest.config.js} +0 -0
@@ -0,0 +1,447 @@
1
+ import { beforeEach, describe, expect, it } from 'vitest';
2
+ import modelService from './index.js';
3
+ import Model from './model.js';
4
+
5
+ // helper tokenizer
6
+ const tokenizer = (t) => t.split(' ');
7
+
8
+ describe('Model negotiation', () => {
9
+ beforeEach(() => {
10
+ // Reset models before each test
11
+ modelService.models = {};
12
+ modelService.bestPublicModelKey = 'fastGood';
13
+ });
14
+
15
+ describe('Privacy models', () => {
16
+ it('prefers privacy model when requested and available', () => {
17
+ modelService.models = {
18
+ privacy: new Model({
19
+ name: 'privacy-model',
20
+ maxContextWindow: 128000,
21
+ maxOutputTokens: 8192,
22
+ requestTimeout: 1000,
23
+ tokenizer,
24
+ }),
25
+ fastGood: new Model({
26
+ name: 'fast-good-model',
27
+ maxContextWindow: 128000,
28
+ maxOutputTokens: 16384,
29
+ requestTimeout: 1000,
30
+ tokenizer,
31
+ }),
32
+ };
33
+
34
+ const key = modelService.negotiateModel('fastGood', { privacy: true });
35
+ expect(key).toBe('privacy');
36
+ });
37
+
38
+ it('throws error when privacy requested but not configured', () => {
39
+ modelService.models = {
40
+ fastGood: new Model({
41
+ name: 'fast-good-model',
42
+ maxContextWindow: 128000,
43
+ maxOutputTokens: 16384,
44
+ requestTimeout: 1000,
45
+ tokenizer,
46
+ }),
47
+ };
48
+
49
+ expect(modelService.negotiateModel('fastGood', { privacy: true })).toBe(undefined);
50
+ });
51
+ });
52
+
53
+ describe('Exact model key matching', () => {
54
+ beforeEach(() => {
55
+ modelService.models = {
56
+ fastGood: new Model({
57
+ name: 'fast-good',
58
+ maxContextWindow: 128000,
59
+ maxOutputTokens: 16384,
60
+ requestTimeout: 1000,
61
+ tokenizer,
62
+ }),
63
+ fastCheap: new Model({
64
+ name: 'fast-cheap',
65
+ maxContextWindow: 128000,
66
+ maxOutputTokens: 8192,
67
+ requestTimeout: 1000,
68
+ tokenizer,
69
+ }),
70
+ fastReasoning: new Model({
71
+ name: 'fast-reasoning',
72
+ maxContextWindow: 200000,
73
+ maxOutputTokens: 100000,
74
+ requestTimeout: 1000,
75
+ tokenizer,
76
+ }),
77
+ fastCheapReasoning: new Model({
78
+ name: 'fast-cheap-reasoning',
79
+ maxContextWindow: 128000,
80
+ maxOutputTokens: 16384,
81
+ requestTimeout: 1000,
82
+ tokenizer,
83
+ }),
84
+ fastGoodMulti: new Model({
85
+ name: 'fast-good-multi',
86
+ maxContextWindow: 1000000,
87
+ maxOutputTokens: 32768,
88
+ requestTimeout: 1000,
89
+ tokenizer,
90
+ }),
91
+ fastCheapReasoningMulti: new Model({
92
+ name: 'fast-cheap-reasoning-multi',
93
+ maxContextWindow: 128000,
94
+ maxOutputTokens: 16384,
95
+ requestTimeout: 1000,
96
+ tokenizer,
97
+ }),
98
+ good: new Model({
99
+ name: 'good',
100
+ maxContextWindow: 128000,
101
+ maxOutputTokens: 16384,
102
+ requestTimeout: 1000,
103
+ tokenizer,
104
+ }),
105
+ reasoning: new Model({
106
+ name: 'reasoning',
107
+ maxContextWindow: 200000,
108
+ maxOutputTokens: 100000,
109
+ requestTimeout: 1000,
110
+ tokenizer,
111
+ }),
112
+ fast: new Model({
113
+ name: 'fast',
114
+ maxContextWindow: 128000,
115
+ maxOutputTokens: 16384,
116
+ requestTimeout: 1000,
117
+ tokenizer,
118
+ }),
119
+ cheap: new Model({
120
+ name: 'cheap',
121
+ maxContextWindow: 128000,
122
+ maxOutputTokens: 8192,
123
+ requestTimeout: 1000,
124
+ tokenizer,
125
+ }),
126
+ };
127
+ });
128
+
129
+ it('returns highest priority match for fast + cheap', () => {
130
+ // Should find fastCheap since it has both fast and cheap
131
+ const key = modelService.negotiateModel(null, { fast: true, cheap: true });
132
+ expect(key).toBe('fastCheap'); // fastCheap matches both requirements
133
+ });
134
+
135
+ it('returns highest priority match for fast + reasoning', () => {
136
+ // Should find fastReasoning since it has both fast and reasoning
137
+ const key = modelService.negotiateModel(null, { fast: true, reasoning: true });
138
+ expect(key).toBe('fastCheapReasoning'); // fastReasoning matches both requirements
139
+ });
140
+
141
+ it('returns exact match when it exists and has high priority', () => {
142
+ const key = modelService.negotiateModel(null, { fast: true, cheap: true, reasoning: true });
143
+ expect(key).toBe('fastCheapReasoning'); // Exact match exists
144
+ });
145
+
146
+ it('returns exact match for fast + good + multi', () => {
147
+ const key = modelService.negotiateModel(null, { fast: true, good: true, multi: true });
148
+ expect(key).toBe('fastGoodMulti'); // Exact match exists
149
+ });
150
+
151
+ it('returns exact match for fast + cheap + reasoning + multi', () => {
152
+ const key = modelService.negotiateModel(null, {
153
+ fast: true,
154
+ cheap: true,
155
+ reasoning: true,
156
+ multi: true,
157
+ });
158
+ expect(key).toBe('fastCheapReasoningMulti'); // Exact match exists
159
+ });
160
+ });
161
+
162
+ describe('Fallback logic', () => {
163
+ beforeEach(() => {
164
+ modelService.models = {
165
+ fastGoodCheap: new Model({
166
+ name: 'fast-good-cheap',
167
+ maxContextWindow: 128000,
168
+ maxOutputTokens: 16384,
169
+ requestTimeout: 1000,
170
+ tokenizer,
171
+ }),
172
+ fastGood: new Model({
173
+ name: 'fast-good',
174
+ maxContextWindow: 128000,
175
+ maxOutputTokens: 16384,
176
+ requestTimeout: 1000,
177
+ tokenizer,
178
+ }),
179
+ fastCheap: new Model({
180
+ name: 'fast-cheap',
181
+ maxContextWindow: 128000,
182
+ maxOutputTokens: 8192,
183
+ requestTimeout: 1000,
184
+ tokenizer,
185
+ }),
186
+ fastReasoning: new Model({
187
+ name: 'fast-reasoning',
188
+ maxContextWindow: 200000,
189
+ maxOutputTokens: 100000,
190
+ requestTimeout: 1000,
191
+ tokenizer,
192
+ }),
193
+ good: new Model({
194
+ name: 'good',
195
+ maxContextWindow: 128000,
196
+ maxOutputTokens: 16384,
197
+ requestTimeout: 1000,
198
+ tokenizer,
199
+ }),
200
+ reasoning: new Model({
201
+ name: 'reasoning',
202
+ maxContextWindow: 200000,
203
+ maxOutputTokens: 100000,
204
+ requestTimeout: 1000,
205
+ tokenizer,
206
+ }),
207
+ fast: new Model({
208
+ name: 'fast',
209
+ maxContextWindow: 128000,
210
+ maxOutputTokens: 16384,
211
+ requestTimeout: 1000,
212
+ tokenizer,
213
+ }),
214
+ cheap: new Model({
215
+ name: 'cheap',
216
+ maxContextWindow: 128000,
217
+ maxOutputTokens: 8192,
218
+ requestTimeout: 1000,
219
+ tokenizer,
220
+ }),
221
+ };
222
+ });
223
+
224
+ it('finds exact match when available', () => {
225
+ const key = modelService.negotiateModel(null, { fast: true, good: true });
226
+ expect(key).toBe('fastGoodCheap');
227
+ });
228
+
229
+ it('requires all specified features to match', () => {
230
+ // Request multi but no multi models exist - should return undefined
231
+ expect(modelService.negotiateModel(null, { fast: true, multi: true })).toBe(undefined);
232
+ });
233
+
234
+ it('picks higher priority model when multiple match', () => {
235
+ // Both fastGoodCheap, fastGood and good match { good: true }, should pick fastGoodCheap (highest priority)
236
+ const key = modelService.negotiateModel(null, { good: true });
237
+ expect(key).toBe('fastGoodCheap');
238
+ });
239
+
240
+ it('respects all requirements strictly', () => {
241
+ // Request fast + cheap + reasoning - no exact match available, should return undefined since reasoning is requested
242
+ expect(modelService.negotiateModel(null, { fast: true, cheap: true, reasoning: true })).toBe(
243
+ undefined
244
+ );
245
+ });
246
+
247
+ it('works with single requirements', () => {
248
+ const key = modelService.negotiateModel(null, { reasoning: true });
249
+ expect(key).toBe('fastReasoning');
250
+ });
251
+
252
+ it('works with multi requirement when available', () => {
253
+ modelService.models.fastGoodMulti = new Model({
254
+ name: 'fast-good-multi',
255
+ maxContextWindow: 1000000,
256
+ maxOutputTokens: 32768,
257
+ requestTimeout: 1000,
258
+ tokenizer,
259
+ });
260
+
261
+ const key = modelService.negotiateModel(null, { fast: true, good: true, multi: true });
262
+ expect(key).toBe('fastGoodMulti');
263
+ });
264
+
265
+ it('falls back to best public model when no matches found', () => {
266
+ // Request something that doesn't exist with reasoning - should return undefined
267
+ expect(
268
+ modelService.negotiateModel(null, {
269
+ fast: true,
270
+ cheap: true,
271
+ good: true,
272
+ reasoning: true,
273
+ multi: true,
274
+ })
275
+ ).toBe(undefined);
276
+ });
277
+
278
+ it('prioritizes better combinations over individual features', () => {
279
+ // Both fast and fastGoodCheap match { fast: true }, should pick fastGoodCheap
280
+ const key = modelService.negotiateModel(null, { fast: true });
281
+ expect(key).toBe('fastGoodCheap');
282
+ });
283
+ });
284
+
285
+ describe('Preferred model handling', () => {
286
+ beforeEach(() => {
287
+ modelService.models = {
288
+ fastGood: new Model({
289
+ name: 'fast-good',
290
+ maxContextWindow: 128000,
291
+ maxOutputTokens: 16384,
292
+ requestTimeout: 1000,
293
+ tokenizer,
294
+ }),
295
+ customModel: new Model({
296
+ name: 'custom',
297
+ maxContextWindow: 64000,
298
+ maxOutputTokens: 8192,
299
+ requestTimeout: 1000,
300
+ tokenizer,
301
+ }),
302
+ };
303
+ });
304
+
305
+ it('returns preferred model when available and no privacy requested', () => {
306
+ const key = modelService.negotiateModel('customModel', { fast: true });
307
+ expect(key).toBe('fastGood');
308
+ });
309
+
310
+ it('ignores preferred model when privacy is requested', () => {
311
+ modelService.models.privacy = new Model({
312
+ name: 'privacy',
313
+ maxContextWindow: 128000,
314
+ maxOutputTokens: 8192,
315
+ requestTimeout: 1000,
316
+ tokenizer,
317
+ });
318
+
319
+ const key = modelService.negotiateModel('customModel', { privacy: true });
320
+ expect(key).toBe('privacy');
321
+ });
322
+
323
+ it('falls back to negotiation when preferred model does not exist', () => {
324
+ const key = modelService.negotiateModel('nonExistentModel', { fast: true });
325
+ expect(key).toBe('fastGood');
326
+ });
327
+ });
328
+
329
+ describe('Edge cases', () => {
330
+ beforeEach(() => {
331
+ modelService.models = {
332
+ fastGood: new Model({
333
+ name: 'fast-good',
334
+ maxContextWindow: 128000,
335
+ maxOutputTokens: 16384,
336
+ requestTimeout: 1000,
337
+ tokenizer,
338
+ }),
339
+ };
340
+ });
341
+
342
+ it('handles empty negotiation object', () => {
343
+ const key = modelService.negotiateModel(null, {});
344
+ expect(key).toBe('fastGood');
345
+ });
346
+
347
+ it('handles undefined negotiation', () => {
348
+ const key = modelService.negotiateModel(null);
349
+ expect(key).toBe('fastGood');
350
+ });
351
+
352
+ it('handles single flag requests', () => {
353
+ const key = modelService.negotiateModel(null, { fast: true });
354
+ expect(key).toBe('fastGood');
355
+ });
356
+
357
+ it('prioritizes reasoning over good when both are requested', () => {
358
+ modelService.models.fastReasoning = new Model({
359
+ name: 'fast-reasoning',
360
+ maxContextWindow: 200000,
361
+ maxOutputTokens: 100000,
362
+ requestTimeout: 1000,
363
+ tokenizer,
364
+ });
365
+
366
+ // Request fast + reasoning + good but no model has all three - should return undefined
367
+ expect(modelService.negotiateModel(null, { fast: true, reasoning: true, good: true })).toBe(
368
+ undefined
369
+ );
370
+ });
371
+ });
372
+
373
+ describe('Property negation', () => {
374
+ beforeEach(() => {
375
+ modelService.models = {
376
+ fastGoodCheap: new Model({
377
+ name: 'fast-good-cheap',
378
+ maxContextWindow: 128000,
379
+ maxOutputTokens: 16384,
380
+ requestTimeout: 1000,
381
+ tokenizer,
382
+ }),
383
+ fastGood: new Model({
384
+ name: 'fast-good',
385
+ maxContextWindow: 128000,
386
+ maxOutputTokens: 16384,
387
+ requestTimeout: 1000,
388
+ tokenizer,
389
+ }),
390
+ good: new Model({
391
+ name: 'good',
392
+ maxContextWindow: 128000,
393
+ maxOutputTokens: 16384,
394
+ requestTimeout: 1000,
395
+ tokenizer,
396
+ }),
397
+ fast: new Model({
398
+ name: 'fast',
399
+ maxContextWindow: 128000,
400
+ maxOutputTokens: 16384,
401
+ requestTimeout: 1000,
402
+ tokenizer,
403
+ }),
404
+ reasoning: new Model({
405
+ name: 'reasoning',
406
+ maxContextWindow: 200000,
407
+ maxOutputTokens: 100000,
408
+ requestTimeout: 1000,
409
+ tokenizer,
410
+ }),
411
+ };
412
+ });
413
+
414
+ it('excludes models with negated properties', () => {
415
+ // Request good but NOT fast - should pick 'good' over 'fastGood'
416
+ const key = modelService.negotiateModel(null, { good: true, fast: false });
417
+ expect(key).toBe('good');
418
+ });
419
+
420
+ it('excludes models with multiple negated properties', () => {
421
+ // Request good but NOT fast and NOT cheap - should pick 'good'
422
+ const key = modelService.negotiateModel(null, { good: true, fast: false, cheap: false });
423
+ expect(key).toBe('good');
424
+ });
425
+
426
+ it('works with only negated properties', () => {
427
+ // Request NOT fast - should pick 'good' (first non-fast model in priority)
428
+ const key = modelService.negotiateModel(null, { fast: false });
429
+ expect(key).toBe('good');
430
+ });
431
+
432
+ it('combines positive and negative requirements', () => {
433
+ // Request fast but NOT good - should pick 'fast' over 'fastGood'
434
+ // fastGoodCheap has both fast and good, so it's excluded by good: false
435
+ // fastGood has both fast and good, so it's excluded by good: false
436
+ // fast has fast but no good, so it matches
437
+ const key = modelService.negotiateModel(null, { fast: true, good: false });
438
+ expect(key).toBe('fast');
439
+ });
440
+
441
+ it('falls back when negation eliminates all matches', () => {
442
+ // Request NOT reasoning (all models are non-reasoning, so should pick first priority)
443
+ const key = modelService.negotiateModel(null, { good: false });
444
+ expect(key).toBe('fast');
445
+ });
446
+ });
447
+ });
@@ -8,25 +8,84 @@ class NullRedisClient {
8
8
  this.store = {};
9
9
  }
10
10
 
11
- async get(key) {
11
+ get(key) {
12
12
  // Redis returns null, not undefined
13
13
  return this.store[key] ?? null;
14
14
  }
15
15
 
16
- async del(key) {
16
+ del(key) {
17
17
  delete this.store[key];
18
18
  }
19
19
 
20
- async set(key, value) {
20
+ set(key, value, _options) {
21
21
  this.store[key] = value;
22
22
  }
23
23
 
24
- // eslint-disable-next-line class-methods-use-this, no-empty-function
25
- async disconnect() {
24
+ disconnect() {
26
25
  // no implementation
27
26
  }
28
27
  }
29
28
 
29
+ class SafeRedisClient {
30
+ constructor(redisClient) {
31
+ this.redisClient = redisClient;
32
+ this.fallbackClient = new NullRedisClient();
33
+ }
34
+
35
+ async get(key) {
36
+ try {
37
+ return await this.redisClient.get(key);
38
+ } catch (error) {
39
+ if (this.isConnectionError(error)) {
40
+ console.warn('Redis connection lost, falling back to in-memory cache');
41
+ return this.fallbackClient.get(key);
42
+ }
43
+ throw error;
44
+ }
45
+ }
46
+
47
+ async set(key, value, options) {
48
+ try {
49
+ return await this.redisClient.set(key, value, options);
50
+ } catch (error) {
51
+ if (this.isConnectionError(error)) {
52
+ console.warn('Redis connection lost, falling back to in-memory cache');
53
+ return this.fallbackClient.set(key, value, options);
54
+ }
55
+ throw error;
56
+ }
57
+ }
58
+
59
+ async del(key) {
60
+ try {
61
+ return await this.redisClient.del(key);
62
+ } catch (error) {
63
+ if (this.isConnectionError(error)) {
64
+ console.warn('Redis connection lost, falling back to in-memory cache');
65
+ return this.fallbackClient.del(key);
66
+ }
67
+ throw error;
68
+ }
69
+ }
70
+
71
+ async disconnect() {
72
+ try {
73
+ return await this.redisClient.disconnect();
74
+ } catch {
75
+ // Ignore disconnect errors
76
+ }
77
+ }
78
+
79
+ isConnectionError(error) {
80
+ return (
81
+ error.message.includes('client is closed') ||
82
+ error.message.includes('ECONNREFUSED') ||
83
+ error.message.includes('connection') ||
84
+ error.code === 'ECONNREFUSED'
85
+ );
86
+ }
87
+ }
88
+
30
89
  const constructClient = async () => {
31
90
  if (process.env.TEST === 'true' && process.env.EXAMPLES !== 'true') {
32
91
  client = new NullRedisClient();
@@ -50,14 +109,18 @@ const constructClient = async () => {
50
109
  client = new NullRedisClient();
51
110
  } else {
52
111
  console.error(`Redis service [error]: ${error.message}`);
112
+ client = new NullRedisClient();
53
113
  }
54
114
 
55
- redisClient.disconnect();
115
+ // Safely disconnect the Redis client
116
+ redisClient.disconnect().catch(() => {
117
+ // Ignore disconnect errors
118
+ });
56
119
  });
57
120
 
58
121
  try {
59
122
  await redisClient.connect();
60
- client = redisClient;
123
+ client = new SafeRedisClient(redisClient);
61
124
  } catch (error) {
62
125
  console.error(
63
126
  `Redis service create [warning]: "${error.message}" Falling back to mock Redis client. This may incur greater usage costs and have slower response times.`
@@ -0,0 +1,20 @@
1
+ import { afterAll, beforeEach } from 'vitest';
2
+ import dotenv from 'dotenv';
3
+ import { getClient as getRedis } from '../services/redis/index.js';
4
+
5
+ // Load environment variables from .env file
6
+ dotenv.config();
7
+
8
+ let redisClient;
9
+
10
+ beforeEach(async () => {
11
+ // Get a fresh Redis client for each test
12
+ redisClient = await getRedis();
13
+ });
14
+
15
+ afterAll(async () => {
16
+ // Clean up Redis client after all tests
17
+ if (redisClient) {
18
+ await redisClient.disconnect();
19
+ }
20
+ });
@@ -0,0 +1,26 @@
1
+ # Verblets
2
+
3
+ The `verblets` directory contains individual utilities that wrap specific language-model workflows. Each verblet exports a single function and usually includes its own examples, tests and optional JSON schema.
4
+
5
+ Available verblets:
6
+
7
+ - [auto](./auto)
8
+ - [bool](./bool)
9
+ - [name](./name) - generate evocative names from text
10
+ - [enum](./enum)
11
+ - [intent](./intent)
12
+ - [number](./number)
13
+ - [number-with-units](./number-with-units)
14
+ - [date](../chains/date)
15
+ - [sentiment](./sentiment) - classify text sentiment
16
+ - [schema-org](./schema-org)
17
+ - [name-similar-to](./name-similar-to) - suggest short names matching a style
18
+ - [name](./name) - name something from a definition or description
19
+ - [to-object](./to-object) – see its [README](./to-object/README.md) for details.
20
+ - [list-map](./list-map) - map lists with prompts
21
+ - [list-reduce](./list-reduce) - reduce lists prompts
22
+ - [list-filter](./list-filter) - filter lists with custom instructions
23
+ - [list-group](./list-group) - group lists into categories with prompts
24
+ - [list-expand](./list-expand) - expand lists with similar items with prompts
25
+
26
+ Use these modules directly or compose them inside [chains](../chains/README.md).
@@ -1,27 +1,30 @@
1
- import { describe, expect, it, vi } from 'vitest';
1
+ import { describe, expect, it } from 'vitest';
2
2
 
3
3
  import auto from './index.js';
4
4
 
5
5
  const examples = [
6
6
  {
7
7
  inputs: { text: 'test' },
8
- want: { result: true }
9
- }
8
+ want: {
9
+ typeOfResult: 'object',
10
+ hasProperties: ['functionArgsAsArray'],
11
+ },
12
+ },
10
13
  ];
11
14
 
12
15
  describe('Auto verblet', () => {
13
16
  examples.forEach((example) => {
14
17
  it(example.inputs.text, async () => {
15
- const result = await auto(example.inputs.text)
18
+ const result = await auto(example.inputs.text);
16
19
 
17
20
  if (example.want.typeOfResult) {
18
- expect(typeof result)
19
- .toStrictEqual(example.want.typeOfResult);
21
+ expect(typeof result).toStrictEqual(example.want.typeOfResult);
20
22
  }
21
23
 
22
- if (example.want.result) {
23
- expect(result)
24
- .toStrictEqual(example.want.result);
24
+ if (example.want.hasProperties) {
25
+ example.want.hasProperties.forEach((prop) => {
26
+ expect(result).toHaveProperty(prop);
27
+ });
25
28
  }
26
29
  });
27
30
  });
@@ -1,20 +1,20 @@
1
1
  import chatGPT from '../../lib/chatgpt/index.js';
2
- import toObject from '../../verblets/to-object/index.js';
3
- import schemas from '../../json-schemas/index.js'
2
+ import schemas from '../../json-schemas/index.js';
4
3
 
5
- export default async (text, options={}) => {
6
- const tools = schemas
7
- .map(schema => ({
8
- type: 'function',
9
- function: schema
10
- }))
4
+ export default async (text, config = {}) => {
5
+ const { llm, ...options } = config;
6
+ const tools = schemas.map((schema) => ({
7
+ type: 'function',
8
+ function: schema,
9
+ }));
11
10
 
12
11
  const functionFound = await chatGPT(text, {
13
12
  modelOptions: {
14
13
  // toolChoice: 'auto' // by default
15
14
  tools,
15
+ ...llm,
16
16
  },
17
- ...options
17
+ ...options,
18
18
  });
19
19
 
20
20
  const functionArgs = functionFound.arguments;
@@ -23,6 +23,6 @@ export default async (text, options={}) => {
23
23
 
24
24
  return {
25
25
  ...functionFound,
26
- functionArgsAsArray
26
+ functionArgsAsArray,
27
27
  };
28
28
  };