@remnic/core 10.0.25 → 10.0.27

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 (295) hide show
  1. package/dist/access-admin-ops-surface.js +37 -36
  2. package/dist/access-authorization-probe.js +38 -37
  3. package/dist/access-boundary.js +37 -36
  4. package/dist/access-cli.js +100 -99
  5. package/dist/access-cli.js.map +1 -1
  6. package/dist/access-extraction-force-flush.js +37 -36
  7. package/dist/access-http-briefing.js +38 -37
  8. package/dist/access-http-query.js +39 -38
  9. package/dist/access-http.js +56 -55
  10. package/dist/access-identity-continuity-surface.js +37 -36
  11. package/dist/access-lcm-surface.js +37 -36
  12. package/dist/access-mcp.js +48 -47
  13. package/dist/access-namespace-preflight.js +37 -36
  14. package/dist/access-observe-write-category.js +4 -4
  15. package/dist/access-observe-write-surface.js +37 -36
  16. package/dist/access-operations-batch.js +38 -37
  17. package/dist/access-operations.js +45 -44
  18. package/dist/access-recall-concurrency.js +37 -36
  19. package/dist/access-recall-diagnostics.js +2 -2
  20. package/dist/access-recall-response.js +37 -36
  21. package/dist/access-recall-surface.js +37 -36
  22. package/dist/access-schema.js +3 -3
  23. package/dist/access-service.js +37 -36
  24. package/dist/access-surface-catalog.js +39 -38
  25. package/dist/access-surface-catalog.js.map +1 -1
  26. package/dist/active-recall.js +7 -7
  27. package/dist/{auto-sync-7WWAKW6F.js → auto-sync-UQESPLCB.js} +5 -5
  28. package/dist/briefing.js +7 -7
  29. package/dist/buffer.js +3 -3
  30. package/dist/calibration.js +5 -4
  31. package/dist/calibration.js.map +1 -1
  32. package/dist/causal-consolidation.js +8 -8
  33. package/dist/{chunk-AAQA6ZUP.js → chunk-23ZLN5X6.js} +13 -2
  34. package/dist/{chunk-AAQA6ZUP.js.map → chunk-23ZLN5X6.js.map} +1 -1
  35. package/dist/{chunk-OFQOOLQQ.js → chunk-27LPB4NS.js} +2 -2
  36. package/dist/{chunk-YW4ZYASS.js → chunk-27SAUQ5R.js} +2 -2
  37. package/dist/{chunk-2JGBX5XY.js → chunk-2HLMFRZ7.js} +2 -2
  38. package/dist/{chunk-DJMGVSST.js → chunk-33UAWCBN.js} +2 -2
  39. package/dist/{chunk-UHBZK3CK.js → chunk-36DZ242S.js} +2 -2
  40. package/dist/chunk-3VRMUNWL.js +289 -0
  41. package/dist/chunk-3VRMUNWL.js.map +1 -0
  42. package/dist/{chunk-466EGMFQ.js → chunk-42KSEX6C.js} +1 -1
  43. package/dist/chunk-42KSEX6C.js.map +1 -0
  44. package/dist/{chunk-LT4SMGE4.js → chunk-4WT6I4VG.js} +2 -2
  45. package/dist/{chunk-ML7ZJAKJ.js → chunk-5HT35H4L.js} +4 -4
  46. package/dist/{chunk-LO76EF3Y.js → chunk-7EJCAFMS.js} +2 -2
  47. package/dist/{chunk-YRKKW4KT.js → chunk-7IOB5F7Y.js} +3 -3
  48. package/dist/{chunk-IVUCF3QO.js → chunk-7RTNJ6M2.js} +2 -2
  49. package/dist/{chunk-VPXIKBPX.js → chunk-A3IURDU2.js} +22 -1
  50. package/dist/chunk-A3IURDU2.js.map +1 -0
  51. package/dist/{chunk-YUZRI3QU.js → chunk-AN3G5DZH.js} +2 -2
  52. package/dist/{chunk-N36PBG4A.js → chunk-AQ52ZFBC.js} +2 -2
  53. package/dist/{chunk-O6ADSFGY.js → chunk-AYHPSYZI.js} +2 -2
  54. package/dist/{chunk-FT4UQYOV.js → chunk-BDLOEODO.js} +2 -2
  55. package/dist/{chunk-GXVLEXIX.js → chunk-BDQMMX3M.js} +2 -2
  56. package/dist/{chunk-VOEBHBWG.js → chunk-BHQPOCQE.js} +2 -2
  57. package/dist/{chunk-V4MB5ONM.js → chunk-BWXKJDDH.js} +4 -4
  58. package/dist/{chunk-2I3OREMR.js → chunk-CCDHRDQ3.js} +2 -2
  59. package/dist/{chunk-XKPLERHV.js → chunk-COGLFRXT.js} +37 -37
  60. package/dist/{chunk-EP6BA7VV.js → chunk-CU5TILGD.js} +2 -5
  61. package/dist/chunk-CU5TILGD.js.map +1 -0
  62. package/dist/{chunk-BI3CIDIH.js → chunk-DF2G4IXN.js} +3 -3
  63. package/dist/{chunk-33G67YZF.js → chunk-DWL6YIZH.js} +2 -2
  64. package/dist/{chunk-W3GFSWKE.js → chunk-EET24NSB.js} +8 -8
  65. package/dist/{chunk-2DT7PIDZ.js → chunk-EPOYPPOW.js} +2 -2
  66. package/dist/{chunk-W7NVMERN.js → chunk-ERAVI25Z.js} +2 -2
  67. package/dist/{chunk-RYYVMZBR.js → chunk-ETPIYK4J.js} +2 -2
  68. package/dist/{chunk-ZDUMF4H6.js → chunk-FFGHFU7A.js} +14 -14
  69. package/dist/{chunk-DYJHGJZ4.js → chunk-FJJWA7NL.js} +55 -7
  70. package/dist/chunk-FJJWA7NL.js.map +1 -0
  71. package/dist/{chunk-ZUCNOTNF.js → chunk-G6XEYFCO.js} +14 -14
  72. package/dist/{chunk-NFB5QTOH.js → chunk-GCZHKBY2.js} +2 -2
  73. package/dist/{chunk-JNLEJE4A.js → chunk-GEHIRZXF.js} +2 -2
  74. package/dist/{chunk-VUBVNY3W.js → chunk-GMZNJZRP.js} +2 -2
  75. package/dist/{chunk-RTV3DB4I.js → chunk-H2X22KZW.js} +2 -2
  76. package/dist/{chunk-VYVQZRXF.js → chunk-H3AW6QZF.js} +2 -2
  77. package/dist/{chunk-RQMSD4CN.js → chunk-H75L4ZOH.js} +59 -59
  78. package/dist/{chunk-ZDDDWTC7.js → chunk-HJQXHLQQ.js} +2 -2
  79. package/dist/{chunk-PDFAX4PK.js → chunk-HNEOLXRF.js} +2 -2
  80. package/dist/{chunk-5JXNXKKV.js → chunk-I27WOMJ7.js} +2 -2
  81. package/dist/{chunk-LDZD3OQG.js → chunk-IHGLQJW6.js} +2 -2
  82. package/dist/{chunk-D42L5SQF.js → chunk-IHS2UGYM.js} +6 -6
  83. package/dist/{chunk-VVSPHTSU.js → chunk-JHQAXATW.js} +2 -2
  84. package/dist/{chunk-6VWCL2IZ.js → chunk-JX32ROLZ.js} +3 -3
  85. package/dist/{chunk-OPNSIFFU.js → chunk-JYZLDZ2R.js} +8 -8
  86. package/dist/{chunk-2C5YAVHA.js → chunk-JZIZBJCW.js} +2 -2
  87. package/dist/{chunk-7LCGFUMB.js → chunk-KQAFEZQX.js} +4 -4
  88. package/dist/{chunk-CA2R2AD4.js → chunk-L3IP56F4.js} +38 -38
  89. package/dist/{chunk-32FVH5OQ.js → chunk-L5TJWQEF.js} +6 -6
  90. package/dist/{chunk-PIN5OPBA.js → chunk-LVU7FP27.js} +1 -1
  91. package/dist/{chunk-QM2SPLKQ.js → chunk-MLED254J.js} +1 -1
  92. package/dist/chunk-MLED254J.js.map +1 -0
  93. package/dist/{chunk-5NO42IYF.js → chunk-MPZYCRC6.js} +4 -4
  94. package/dist/{chunk-A2IJEV5L.js → chunk-MSMGQDVW.js} +2 -2
  95. package/dist/{chunk-FBNL4BYD.js → chunk-N34ZW6E4.js} +2 -2
  96. package/dist/{chunk-Q35L7TYB.js → chunk-NYPQHFC3.js} +2 -2
  97. package/dist/{chunk-G2OYFTUD.js → chunk-O6S5FLZB.js} +2 -2
  98. package/dist/{chunk-SSUTJLCF.js → chunk-PLD6SOU7.js} +2 -2
  99. package/dist/{chunk-YBO2WGCV.js → chunk-PM72NSEW.js} +2 -2
  100. package/dist/{chunk-DUPHIZBY.js → chunk-PUDYANAX.js} +2 -2
  101. package/dist/{chunk-62CRBI5B.js → chunk-PXTLPC3P.js} +2 -2
  102. package/dist/{chunk-AAQ77A44.js → chunk-QNKEMFZY.js} +2 -2
  103. package/dist/{chunk-3F3HA3R2.js → chunk-RN7HPFYS.js} +2 -2
  104. package/dist/{chunk-NIS3FOMD.js → chunk-S36KJNQH.js} +2 -2
  105. package/dist/{chunk-QC5TJUSP.js → chunk-SU5V2LWR.js} +2 -2
  106. package/dist/{chunk-INBB7D3V.js → chunk-SYUNCM26.js} +2 -2
  107. package/dist/{chunk-TKC5LZ43.js → chunk-UIDWASOS.js} +2 -2
  108. package/dist/{chunk-UXXLP2PD.js → chunk-UJPGQCXD.js} +3 -3
  109. package/dist/{chunk-EML6HJE6.js → chunk-UKWRCQWT.js} +2 -2
  110. package/dist/{chunk-4OEBL5T3.js → chunk-V7KGDLBC.js} +2 -2
  111. package/dist/{chunk-5FNZ3G6X.js → chunk-VKQI47DJ.js} +4 -4
  112. package/dist/{chunk-TXE55Z3X.js → chunk-VMHCC7LO.js} +2 -2
  113. package/dist/{chunk-HVIXRLXJ.js → chunk-VN3GHPBU.js} +2 -2
  114. package/dist/{chunk-OMERTLB7.js → chunk-W6MOLW5S.js} +9 -7
  115. package/dist/chunk-W6MOLW5S.js.map +1 -0
  116. package/dist/{chunk-IBGRIAE5.js → chunk-WFUO7NWX.js} +2 -2
  117. package/dist/{chunk-3K6J5GO5.js → chunk-WJAGO5LL.js} +3 -3
  118. package/dist/{chunk-CJRENHVZ.js → chunk-XNAOCY67.js} +2 -2
  119. package/dist/{chunk-7UHCV5B3.js → chunk-XSXM5LYZ.js} +2 -2
  120. package/dist/{chunk-PVHQSLNN.js → chunk-Y6IF6H2U.js} +2 -2
  121. package/dist/{chunk-CFZ4SXXH.js → chunk-YEF5AAWN.js} +2 -2
  122. package/dist/{chunk-QZ7WOCME.js → chunk-Z3XVYXQ3.js} +2 -2
  123. package/dist/cli.js +73 -72
  124. package/dist/coding/export-okf-codegraph.js +6 -6
  125. package/dist/compounding/engine.js +8 -8
  126. package/dist/config.d.ts.map +1 -1
  127. package/dist/config.js +7 -7
  128. package/dist/connectors/codex-materialize-runner.js +7 -7
  129. package/dist/connectors/index.js +7 -7
  130. package/dist/convergence-refresh.js +9 -9
  131. package/dist/corpus-watermark.js +9 -9
  132. package/dist/deep-recall-http-glue.js +4 -4
  133. package/dist/deep-recall-policy-llm.js +3 -3
  134. package/dist/entity-retrieval.js +7 -7
  135. package/dist/external-wiki-access.js +38 -37
  136. package/dist/extraction-faithfulness.js +3 -3
  137. package/dist/extraction-judge.js +3 -3
  138. package/dist/extraction-span-wire.d.ts +8 -0
  139. package/dist/extraction-span-wire.d.ts.map +1 -1
  140. package/dist/extraction-span-wire.js +5 -3
  141. package/dist/extraction.js +15 -14
  142. package/dist/fallback-llm-codex-subscription.d.ts.map +1 -1
  143. package/dist/fallback-llm-codex-subscription.js +1 -1
  144. package/dist/fallback-llm.js +2 -2
  145. package/dist/{first-start-migration-QVD22WWS.js → first-start-migration-A6PHIBUQ.js} +4 -4
  146. package/dist/harmonic-retrieval.js +7 -7
  147. package/dist/index.js +113 -112
  148. package/dist/lcm/engine.js +2 -2
  149. package/dist/lcm/index.js +5 -5
  150. package/dist/local-llm-helpers.d.ts +2 -1
  151. package/dist/local-llm-helpers.d.ts.map +1 -1
  152. package/dist/local-llm-helpers.js +1 -1
  153. package/dist/local-llm-rate-limit.d.ts +169 -0
  154. package/dist/local-llm-rate-limit.d.ts.map +1 -0
  155. package/dist/local-llm-rate-limit.js +48 -0
  156. package/dist/local-llm-rate-limit.js.map +1 -0
  157. package/dist/local-llm.d.ts.map +1 -1
  158. package/dist/local-llm.js +7 -2
  159. package/dist/location/index.js +9 -9
  160. package/dist/maintenance/memory-governance.js +7 -7
  161. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +7 -7
  162. package/dist/maintenance/rebuild-memory-projection-runner.js +9 -9
  163. package/dist/maintenance/rebuild-memory-projection.js +8 -8
  164. package/dist/memory-browse-operations.js +38 -37
  165. package/dist/memory-browse.js +3 -3
  166. package/dist/namespaces/migrate.js +8 -8
  167. package/dist/namespaces/storage.js +7 -7
  168. package/dist/operator-doctor-corpus.js +10 -10
  169. package/dist/operator-toolkit.js +16 -16
  170. package/dist/orchestration/maintenance.js +20 -19
  171. package/dist/orchestrator.js +102 -101
  172. package/dist/recall-explain-renderer.js +3 -3
  173. package/dist/recall-navigate-access.js +2 -2
  174. package/dist/recall-navigate-disclosure.js +2 -2
  175. package/dist/recall-navigate.js +2 -2
  176. package/dist/recall-navigation-operations.js +38 -37
  177. package/dist/recall-navigation.js +37 -36
  178. package/dist/recall-planner-llm.js +2 -2
  179. package/dist/recall-state-view-admission.js +3 -3
  180. package/dist/recall-why-service.js +3 -3
  181. package/dist/recall-xray-cli.js +4 -4
  182. package/dist/recall-xray-renderer.js +3 -3
  183. package/dist/recall-xray.js +2 -2
  184. package/dist/resume-bundles.js +8 -8
  185. package/dist/semantic-consolidation.js +8 -8
  186. package/dist/semantic-rule-promotion.js +7 -7
  187. package/dist/semantic-rule-verifier.js +7 -7
  188. package/dist/source-agent-qualifier.js +10 -10
  189. package/dist/standup/build.js +7 -7
  190. package/dist/storage.js +6 -6
  191. package/dist/summarizer.js +6 -5
  192. package/dist/support-passport/index.js +40 -39
  193. package/dist/transfer/capsule-export.js +2 -2
  194. package/dist/transfer/capsule-import.js +2 -2
  195. package/dist/transfer/export-okf.js +7 -7
  196. package/dist/types.d.ts +8 -0
  197. package/dist/types.d.ts.map +1 -1
  198. package/dist/types.js +1 -1
  199. package/dist/verified-recall.js +7 -7
  200. package/dist/who-knows-cli.js +8 -8
  201. package/dist/who-knows.js +7 -7
  202. package/package.json +2 -2
  203. package/src/ambient-provenance.test.ts +21 -3
  204. package/src/config.ts +9 -0
  205. package/src/extraction-span-wire.ts +11 -0
  206. package/src/extraction.ts +2 -2
  207. package/src/fallback-llm-codex-subscription.ts +6 -9
  208. package/src/local-llm-helpers.ts +2 -1
  209. package/src/local-llm-json-schema-fallback.test.ts +278 -0
  210. package/src/local-llm-rate-limit.test.ts +406 -0
  211. package/src/local-llm-rate-limit.ts +418 -0
  212. package/src/local-llm.ts +57 -5
  213. package/src/types.ts +8 -0
  214. package/dist/chunk-466EGMFQ.js.map +0 -1
  215. package/dist/chunk-DYJHGJZ4.js.map +0 -1
  216. package/dist/chunk-EP6BA7VV.js.map +0 -1
  217. package/dist/chunk-OMERTLB7.js.map +0 -1
  218. package/dist/chunk-QM2SPLKQ.js.map +0 -1
  219. package/dist/chunk-VPXIKBPX.js.map +0 -1
  220. /package/dist/{auto-sync-7WWAKW6F.js.map → auto-sync-UQESPLCB.js.map} +0 -0
  221. /package/dist/{chunk-OFQOOLQQ.js.map → chunk-27LPB4NS.js.map} +0 -0
  222. /package/dist/{chunk-YW4ZYASS.js.map → chunk-27SAUQ5R.js.map} +0 -0
  223. /package/dist/{chunk-2JGBX5XY.js.map → chunk-2HLMFRZ7.js.map} +0 -0
  224. /package/dist/{chunk-DJMGVSST.js.map → chunk-33UAWCBN.js.map} +0 -0
  225. /package/dist/{chunk-UHBZK3CK.js.map → chunk-36DZ242S.js.map} +0 -0
  226. /package/dist/{chunk-LT4SMGE4.js.map → chunk-4WT6I4VG.js.map} +0 -0
  227. /package/dist/{chunk-ML7ZJAKJ.js.map → chunk-5HT35H4L.js.map} +0 -0
  228. /package/dist/{chunk-LO76EF3Y.js.map → chunk-7EJCAFMS.js.map} +0 -0
  229. /package/dist/{chunk-YRKKW4KT.js.map → chunk-7IOB5F7Y.js.map} +0 -0
  230. /package/dist/{chunk-IVUCF3QO.js.map → chunk-7RTNJ6M2.js.map} +0 -0
  231. /package/dist/{chunk-YUZRI3QU.js.map → chunk-AN3G5DZH.js.map} +0 -0
  232. /package/dist/{chunk-N36PBG4A.js.map → chunk-AQ52ZFBC.js.map} +0 -0
  233. /package/dist/{chunk-O6ADSFGY.js.map → chunk-AYHPSYZI.js.map} +0 -0
  234. /package/dist/{chunk-FT4UQYOV.js.map → chunk-BDLOEODO.js.map} +0 -0
  235. /package/dist/{chunk-GXVLEXIX.js.map → chunk-BDQMMX3M.js.map} +0 -0
  236. /package/dist/{chunk-VOEBHBWG.js.map → chunk-BHQPOCQE.js.map} +0 -0
  237. /package/dist/{chunk-V4MB5ONM.js.map → chunk-BWXKJDDH.js.map} +0 -0
  238. /package/dist/{chunk-2I3OREMR.js.map → chunk-CCDHRDQ3.js.map} +0 -0
  239. /package/dist/{chunk-XKPLERHV.js.map → chunk-COGLFRXT.js.map} +0 -0
  240. /package/dist/{chunk-BI3CIDIH.js.map → chunk-DF2G4IXN.js.map} +0 -0
  241. /package/dist/{chunk-33G67YZF.js.map → chunk-DWL6YIZH.js.map} +0 -0
  242. /package/dist/{chunk-W3GFSWKE.js.map → chunk-EET24NSB.js.map} +0 -0
  243. /package/dist/{chunk-2DT7PIDZ.js.map → chunk-EPOYPPOW.js.map} +0 -0
  244. /package/dist/{chunk-W7NVMERN.js.map → chunk-ERAVI25Z.js.map} +0 -0
  245. /package/dist/{chunk-RYYVMZBR.js.map → chunk-ETPIYK4J.js.map} +0 -0
  246. /package/dist/{chunk-ZDUMF4H6.js.map → chunk-FFGHFU7A.js.map} +0 -0
  247. /package/dist/{chunk-ZUCNOTNF.js.map → chunk-G6XEYFCO.js.map} +0 -0
  248. /package/dist/{chunk-NFB5QTOH.js.map → chunk-GCZHKBY2.js.map} +0 -0
  249. /package/dist/{chunk-JNLEJE4A.js.map → chunk-GEHIRZXF.js.map} +0 -0
  250. /package/dist/{chunk-VUBVNY3W.js.map → chunk-GMZNJZRP.js.map} +0 -0
  251. /package/dist/{chunk-RTV3DB4I.js.map → chunk-H2X22KZW.js.map} +0 -0
  252. /package/dist/{chunk-VYVQZRXF.js.map → chunk-H3AW6QZF.js.map} +0 -0
  253. /package/dist/{chunk-RQMSD4CN.js.map → chunk-H75L4ZOH.js.map} +0 -0
  254. /package/dist/{chunk-ZDDDWTC7.js.map → chunk-HJQXHLQQ.js.map} +0 -0
  255. /package/dist/{chunk-PDFAX4PK.js.map → chunk-HNEOLXRF.js.map} +0 -0
  256. /package/dist/{chunk-5JXNXKKV.js.map → chunk-I27WOMJ7.js.map} +0 -0
  257. /package/dist/{chunk-LDZD3OQG.js.map → chunk-IHGLQJW6.js.map} +0 -0
  258. /package/dist/{chunk-D42L5SQF.js.map → chunk-IHS2UGYM.js.map} +0 -0
  259. /package/dist/{chunk-VVSPHTSU.js.map → chunk-JHQAXATW.js.map} +0 -0
  260. /package/dist/{chunk-6VWCL2IZ.js.map → chunk-JX32ROLZ.js.map} +0 -0
  261. /package/dist/{chunk-OPNSIFFU.js.map → chunk-JYZLDZ2R.js.map} +0 -0
  262. /package/dist/{chunk-2C5YAVHA.js.map → chunk-JZIZBJCW.js.map} +0 -0
  263. /package/dist/{chunk-7LCGFUMB.js.map → chunk-KQAFEZQX.js.map} +0 -0
  264. /package/dist/{chunk-CA2R2AD4.js.map → chunk-L3IP56F4.js.map} +0 -0
  265. /package/dist/{chunk-32FVH5OQ.js.map → chunk-L5TJWQEF.js.map} +0 -0
  266. /package/dist/{chunk-PIN5OPBA.js.map → chunk-LVU7FP27.js.map} +0 -0
  267. /package/dist/{chunk-5NO42IYF.js.map → chunk-MPZYCRC6.js.map} +0 -0
  268. /package/dist/{chunk-A2IJEV5L.js.map → chunk-MSMGQDVW.js.map} +0 -0
  269. /package/dist/{chunk-FBNL4BYD.js.map → chunk-N34ZW6E4.js.map} +0 -0
  270. /package/dist/{chunk-Q35L7TYB.js.map → chunk-NYPQHFC3.js.map} +0 -0
  271. /package/dist/{chunk-G2OYFTUD.js.map → chunk-O6S5FLZB.js.map} +0 -0
  272. /package/dist/{chunk-SSUTJLCF.js.map → chunk-PLD6SOU7.js.map} +0 -0
  273. /package/dist/{chunk-YBO2WGCV.js.map → chunk-PM72NSEW.js.map} +0 -0
  274. /package/dist/{chunk-DUPHIZBY.js.map → chunk-PUDYANAX.js.map} +0 -0
  275. /package/dist/{chunk-62CRBI5B.js.map → chunk-PXTLPC3P.js.map} +0 -0
  276. /package/dist/{chunk-AAQ77A44.js.map → chunk-QNKEMFZY.js.map} +0 -0
  277. /package/dist/{chunk-3F3HA3R2.js.map → chunk-RN7HPFYS.js.map} +0 -0
  278. /package/dist/{chunk-NIS3FOMD.js.map → chunk-S36KJNQH.js.map} +0 -0
  279. /package/dist/{chunk-QC5TJUSP.js.map → chunk-SU5V2LWR.js.map} +0 -0
  280. /package/dist/{chunk-INBB7D3V.js.map → chunk-SYUNCM26.js.map} +0 -0
  281. /package/dist/{chunk-TKC5LZ43.js.map → chunk-UIDWASOS.js.map} +0 -0
  282. /package/dist/{chunk-UXXLP2PD.js.map → chunk-UJPGQCXD.js.map} +0 -0
  283. /package/dist/{chunk-EML6HJE6.js.map → chunk-UKWRCQWT.js.map} +0 -0
  284. /package/dist/{chunk-4OEBL5T3.js.map → chunk-V7KGDLBC.js.map} +0 -0
  285. /package/dist/{chunk-5FNZ3G6X.js.map → chunk-VKQI47DJ.js.map} +0 -0
  286. /package/dist/{chunk-TXE55Z3X.js.map → chunk-VMHCC7LO.js.map} +0 -0
  287. /package/dist/{chunk-HVIXRLXJ.js.map → chunk-VN3GHPBU.js.map} +0 -0
  288. /package/dist/{chunk-IBGRIAE5.js.map → chunk-WFUO7NWX.js.map} +0 -0
  289. /package/dist/{chunk-3K6J5GO5.js.map → chunk-WJAGO5LL.js.map} +0 -0
  290. /package/dist/{chunk-CJRENHVZ.js.map → chunk-XNAOCY67.js.map} +0 -0
  291. /package/dist/{chunk-7UHCV5B3.js.map → chunk-XSXM5LYZ.js.map} +0 -0
  292. /package/dist/{chunk-PVHQSLNN.js.map → chunk-Y6IF6H2U.js.map} +0 -0
  293. /package/dist/{chunk-CFZ4SXXH.js.map → chunk-YEF5AAWN.js.map} +0 -0
  294. /package/dist/{chunk-QZ7WOCME.js.map → chunk-Z3XVYXQ3.js.map} +0 -0
  295. /package/dist/{first-start-migration-QVD22WWS.js.map → first-start-migration-A6PHIBUQ.js.map} +0 -0
@@ -1,10 +1,11 @@
1
+ import type { ResponseFormatJSONSchema } from "openai/resources/shared";
1
2
  import { log } from "./logger.js";
2
3
  import type { LocalLlmRequestPriority } from "./local-llm.js";
3
4
 
4
5
  export interface LocalLlmChatCompletionOptions {
5
6
  temperature?: number;
6
7
  maxTokens?: number;
7
- responseFormat?: { type: string };
8
+ responseFormat?: { type: string } | ResponseFormatJSONSchema;
8
9
  timeoutMs?: number;
9
10
  operation?: string;
10
11
  forceDisableThinking?: boolean;
@@ -0,0 +1,278 @@
1
+ import assert from "node:assert/strict";
2
+ import http from "node:http";
3
+ import type { AddressInfo } from "node:net";
4
+ import test, { mock } from "node:test";
5
+
6
+ import { LocalLlmClient } from "./local-llm.js";
7
+ import { ChatTransport } from "./local-llm-transport.js";
8
+ import { parseConfig } from "./config.js";
9
+ import type { LocalLlmChatCompletionOptions } from "./local-llm-helpers.js";
10
+ import type { PluginConfig } from "./types.js";
11
+
12
+ function createConfig(overrides: Partial<PluginConfig> = {}): PluginConfig {
13
+ return parseConfig({
14
+ localLlmEnabled: true,
15
+ localLlmModel: "test-local-model",
16
+ localLlmUrl: "http://127.0.0.1:1234",
17
+ localLlmTimeoutMs: 5_000,
18
+ localLlmRetry5xxCount: 0,
19
+ localLlmRateLimitRetryCount: 0,
20
+ localLlmRetryBackoffMs: 1,
21
+ localLlmHeaders: { "X-Test": "yes" },
22
+ localLlmApiKey: undefined,
23
+ localLlmAuthHeader: false,
24
+ localLlm400TripThreshold: 50,
25
+ localLlm400CooldownMs: 60_000,
26
+ debug: false,
27
+ localLlmReasoningEffort: "none",
28
+ slowLogEnabled: false,
29
+ slowLogThresholdMs: 60_000,
30
+ ...overrides,
31
+ });
32
+ }
33
+
34
+ function primeClient(client: LocalLlmClient): void {
35
+ mock.method(client, "checkAvailability", async () => true);
36
+ }
37
+
38
+ interface RecordedRequest {
39
+ headers: http.IncomingHttpHeaders;
40
+ body: { model: string; response_format?: LocalLlmChatCompletionOptions["responseFormat"] };
41
+ }
42
+
43
+ async function startRecordingServer(
44
+ respond: (req: http.IncomingMessage, requestCount: number) => {
45
+ status: number;
46
+ body: unknown;
47
+ },
48
+ ): Promise<{
49
+ url: string;
50
+ requests: RecordedRequest[];
51
+ close: () => Promise<void>;
52
+ }> {
53
+ const requests: RecordedRequest[] = [];
54
+ const server = http.createServer((req, res) => {
55
+ let raw = "";
56
+ req.setEncoding("utf8");
57
+ req.on("data", (chunk: string) => {
58
+ raw += chunk;
59
+ });
60
+ req.on("end", () => {
61
+ requests.push({ headers: req.headers, body: JSON.parse(raw) });
62
+ const { status, body } = respond(req, requests.length);
63
+ res.writeHead(status, { "content-type": "application/json" });
64
+ res.end(JSON.stringify(body));
65
+ });
66
+ });
67
+ await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
68
+ // SAFETY: listen(0, "127.0.0.1") above binds TCP, so its address is AddressInfo.
69
+ const { port } = server.address() as AddressInfo;
70
+ return {
71
+ url: `http://127.0.0.1:${port}/v1`,
72
+ requests,
73
+ close: async () => {
74
+ await new Promise<void>((resolve) => server.close(() => resolve()));
75
+ },
76
+ };
77
+ }
78
+
79
+ const okCompletion = {
80
+ choices: [{ message: { content: "done" } }],
81
+ usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 },
82
+ };
83
+
84
+ const responseFormat = {
85
+ type: "json_schema",
86
+ json_schema: {
87
+ name: "result",
88
+ schema: { type: "object", properties: { ok: { type: "boolean" } } },
89
+ },
90
+ };
91
+
92
+ test("forwards json_schema response_format when the backend accepts it", async () => {
93
+ const server = await startRecordingServer(() => ({ status: 200, body: okCompletion }));
94
+ try {
95
+ const client = new LocalLlmClient(createConfig({ localLlmUrl: server.url }));
96
+ primeClient(client);
97
+ const result = await client.chatCompletion([{ role: "user", content: "hello" }], {
98
+ responseFormat,
99
+ });
100
+ assert.equal(result?.content, "done");
101
+ assert.equal(server.requests.length, 1);
102
+ assert.deepEqual(server.requests[0].body.response_format, responseFormat);
103
+ } finally {
104
+ await server.close();
105
+ }
106
+ });
107
+
108
+ test("400 rejecting json_schema triggers exactly one retry without the format", async (t) => {
109
+ const server = await startRecordingServer((_req, count) =>
110
+ count === 1
111
+ ? {
112
+ status: 400,
113
+ body: {
114
+ error: { message: "response_format type json_schema is not supported" },
115
+ },
116
+ }
117
+ : { status: 200, body: okCompletion },
118
+ );
119
+ try {
120
+ const client = new LocalLlmClient(createConfig({ localLlmUrl: server.url }));
121
+ primeClient(client);
122
+ const post = ChatTransport.prototype.post;
123
+ const attempts: Array<Parameters<ChatTransport["post"]>[0]> = [];
124
+ t.mock.method(ChatTransport.prototype, "post", async function (this: ChatTransport, request: Parameters<ChatTransport["post"]>[0]) {
125
+ attempts.push(request);
126
+ return post.call(this, request);
127
+ });
128
+ const result = await client.chatCompletion([{ role: "user", content: "hello" }], {
129
+ responseFormat,
130
+ });
131
+ assert.equal(result?.content, "done");
132
+ assert.equal(server.requests.length, 2, "exactly one strip-and-retry");
133
+ assert.deepEqual(server.requests[0].body.response_format, responseFormat);
134
+ assert.equal(server.requests[1].body.response_format, undefined);
135
+ // headers/model/deadline preserved on the retry
136
+ assert.equal(server.requests[1].headers["x-test"], "yes");
137
+ assert.equal(server.requests[1].body.model, "test-local-model");
138
+ assert.equal(attempts[0].signal, attempts[1].signal, "retry shares the attempt deadline and caller abort");
139
+ assert.equal(attempts[0].budgetMs, attempts[1].budgetMs);
140
+ assert.deepEqual(attempts[0].headers, attempts[1].headers);
141
+ } finally {
142
+ await server.close();
143
+ }
144
+ });
145
+
146
+ test("timeout before strip-retry headers uses the configured transport retry", async (t) => {
147
+ const bodies: RecordedRequest["body"][] = [];
148
+ t.mock.method(ChatTransport.prototype, "post", async (request: Parameters<ChatTransport["post"]>[0]) => {
149
+ bodies.push(JSON.parse(request.body));
150
+ if (bodies.length === 1) return new Response("json_schema is not supported", { status: 400 });
151
+ if (bodies.length === 2) throw new DOMException("Timed out before headers", "AbortError");
152
+ return new Response(JSON.stringify(okCompletion), { status: 200 });
153
+ });
154
+ const client = new LocalLlmClient(createConfig({ localLlmRetry5xxCount: 1, localLlm400TripThreshold: 1 }));
155
+ primeClient(client);
156
+ const result = await client.chatCompletion([{ role: "user", content: "hello" }], { responseFormat });
157
+ assert.equal(result?.content, "done");
158
+ assert.equal(bodies.length, 3);
159
+ assert.deepEqual(bodies[0].response_format, responseFormat);
160
+ assert.equal(bodies[1].response_format, undefined);
161
+ assert.equal(bodies[2].response_format, undefined);
162
+ });
163
+
164
+ test("a diagnostic echo of the format is not a schema capability rejection", async () => {
165
+ for (const message of [
166
+ "Context length exceeded; request included response_format json_schema",
167
+ "Unsupported parameter temperature; request included response_format json_schema",
168
+ ]) {
169
+ const server = await startRecordingServer(() => ({ status: 400, body: { error: { message } } }));
170
+ try {
171
+ const client = new LocalLlmClient(createConfig({ localLlmUrl: server.url }));
172
+ primeClient(client);
173
+ assert.equal(await client.chatCompletion([{ role: "user", content: "hello" }], { responseFormat }), null);
174
+ assert.equal(server.requests.length, 1, "unrelated provider errors must not strip the format");
175
+ } finally {
176
+ await server.close();
177
+ }
178
+ }
179
+ });
180
+
181
+ test("an explicit unknown response_format field retries without the format", async () => {
182
+ const server = await startRecordingServer((_req, count) => count === 1
183
+ ? { status: 400, body: { error: { message: "Unknown field response_format" } } }
184
+ : { status: 200, body: okCompletion });
185
+ try {
186
+ const client = new LocalLlmClient(createConfig({ localLlmUrl: server.url }));
187
+ primeClient(client);
188
+ assert.equal((await client.chatCompletion([{ role: "user", content: "hello" }], { responseFormat }))?.content, "done");
189
+ assert.equal(server.requests.length, 2);
190
+ assert.deepEqual(server.requests[0].body.response_format, responseFormat);
191
+ assert.equal(server.requests[1].body.response_format, undefined);
192
+ } finally {
193
+ await server.close();
194
+ }
195
+ });
196
+
197
+ test("format removal persists for later 5xx attempts", async () => {
198
+ const server = await startRecordingServer((_req, count) =>
199
+ count === 1
200
+ ? { status: 400, body: { error: { message: "json_schema unsupported by backend" } } }
201
+ : count === 2
202
+ ? { status: 500, body: { error: { message: "boom" } } }
203
+ : { status: 200, body: okCompletion },
204
+ );
205
+ try {
206
+ const client = new LocalLlmClient(
207
+ createConfig({ localLlmUrl: server.url, localLlmRetry5xxCount: 2 }),
208
+ );
209
+ primeClient(client);
210
+ const result = await client.chatCompletion([{ role: "user", content: "hello" }], {
211
+ responseFormat,
212
+ });
213
+ assert.equal(result?.content, "done");
214
+ assert.equal(server.requests.length, 3);
215
+ assert.equal(server.requests[2].body.response_format, undefined);
216
+ } finally {
217
+ await server.close();
218
+ }
219
+ });
220
+
221
+ test("unrelated 400 and 429 are not retried with a stripped format", async () => {
222
+ for (const status of [400, 429]) {
223
+ const server = await startRecordingServer(() => ({
224
+ status,
225
+ body: { error: { message: "some other problem" } },
226
+ }));
227
+ try {
228
+ const client = new LocalLlmClient(createConfig({ localLlmUrl: server.url }));
229
+ primeClient(client);
230
+ const result = await client.chatCompletion([{ role: "user", content: "hello" }], {
231
+ responseFormat,
232
+ });
233
+ assert.equal(result, null);
234
+ assert.equal(server.requests.length, 1);
235
+ assert.ok(server.requests[0].body.response_format, "format still present");
236
+ } finally {
237
+ await server.close();
238
+ }
239
+ }
240
+ });
241
+
242
+ test("caller abort still fails closed during the strip retry", async () => {
243
+ const controller = new AbortController();
244
+ const server = await startRecordingServer((_req, count) => {
245
+ if (count === 2) controller.abort();
246
+ return count === 1
247
+ ? { status: 400, body: { error: { message: "response_format json_schema is not supported" } } }
248
+ : { status: 200, body: okCompletion };
249
+ });
250
+ try {
251
+ const client = new LocalLlmClient(createConfig({ localLlmUrl: server.url }));
252
+ primeClient(client);
253
+ const pending = client.chatCompletion([{ role: "user", content: "hello" }], {
254
+ responseFormat,
255
+ signal: controller.signal,
256
+ });
257
+ const result = await pending;
258
+ assert.equal(result, null);
259
+ assert.equal(server.requests.length, 2, "caller abort happens during the strip retry, not before the first request");
260
+ } finally {
261
+ await server.close();
262
+ }
263
+ });
264
+
265
+ test("request without response_format is unchanged", async () => {
266
+ const server = await startRecordingServer(() => ({ status: 200, body: okCompletion }));
267
+ try {
268
+ const client = new LocalLlmClient(createConfig({ localLlmUrl: server.url }));
269
+ primeClient(client);
270
+ const result = await client.chatCompletion([{ role: "user", content: "hello" }]);
271
+ assert.equal(result?.content, "done");
272
+ assert.equal(server.requests.length, 1);
273
+ assert.equal(server.requests[0].body.response_format, undefined);
274
+ } finally {
275
+ await server.close();
276
+ }
277
+ });
278
+
@@ -0,0 +1,406 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+
4
+ import { parseConfig } from "./config.js";
5
+ import type { LocalLlmChatCompletionOptions } from "./local-llm-helpers.js";
6
+ import {
7
+ LocalLlmConcurrencyLimiter,
8
+ LocalLlmRateLimitSession,
9
+ computeFullJitterBackoffMs,
10
+ computeRateLimitDelayMs,
11
+ endpointBackoffRemainingMs,
12
+ extendEndpointBackoff,
13
+ isRateLimitedResponse,
14
+ parseRetryAfterMs,
15
+ parseTryAgainHintMs,
16
+ resetLocalLlmRateLimitStateForTests,
17
+ resolveRateLimitSettings,
18
+ } from "./local-llm-rate-limit.js";
19
+ import { LocalLlmClient } from "./local-llm.js";
20
+ import type { PluginConfig } from "./types.js";
21
+
22
+ /**
23
+ * COS-447: a saturated LiteLLM proxy answered `429 No deployments available`
24
+ * and the client returned at once, so caller loops re-fired immediately
25
+ * (2,400+ calls/h). These tests pin backoff, Retry-After, the retry cap, the
26
+ * process-wide endpoint deadline, and the concurrency cap.
27
+ */
28
+
29
+ const LITELLM_429 = JSON.stringify({
30
+ error: {
31
+ message:
32
+ "No deployments available for selected model, Try again in 60 seconds. Passed model=m. pre-call-checks=True, cooldown_list=['a', 'b']",
33
+ },
34
+ });
35
+
36
+ type RateLimitTestOverrides = Partial<Record<"localLlmUrl", string>> &
37
+ Partial<
38
+ Record<
39
+ | "localLlmRetry5xxCount"
40
+ | "localLlmRateLimitRetryCount"
41
+ | "localLlmRateLimitBackoffMaxMs"
42
+ | "localLlmMaxConcurrency",
43
+ number
44
+ >
45
+ >;
46
+
47
+ function createConfig(overrides: RateLimitTestOverrides = {}): PluginConfig {
48
+ return parseConfig({
49
+ memoryDir: "/tmp/remnic-cos-447-rate-limit-test",
50
+ localLlmEnabled: true,
51
+ localLlmModel: "test-local-model",
52
+ localLlmUrl: "http://127.0.0.1:1234",
53
+ localLlmTimeoutMs: 5_000,
54
+ localLlmRetry5xxCount: 0,
55
+ localLlmRetryBackoffMs: 1,
56
+ localLlmAuthHeader: false,
57
+ localLlmRateLimitRetryCount: 3,
58
+ localLlmRateLimitBackoffBaseMs: 1,
59
+ localLlmRateLimitBackoffMaxMs: 20,
60
+ localLlmMaxConcurrency: 4,
61
+ ...overrides,
62
+ });
63
+ }
64
+
65
+ function ok(content = "ok"): Response {
66
+ return new Response(JSON.stringify({ choices: [{ message: { content } }] }), {
67
+ status: 200,
68
+ headers: { "content-type": "application/json" },
69
+ });
70
+ }
71
+
72
+ const DEFAULT_429_BODY = JSON.stringify({ error: { message: "No deployments available for selected model." } });
73
+
74
+ function rateLimited(status = 429, headers: Record<string, string> = {}, body = DEFAULT_429_BODY): Response {
75
+ return new Response(body, { status, headers: { "content-type": "application/json", ...headers } });
76
+ }
77
+
78
+ function modelsResponse(): Response {
79
+ return new Response(JSON.stringify({ object: "list", data: [{ id: "test-local-model", object: "model" }] }), {
80
+ status: 200,
81
+ headers: { "content-type": "application/json" },
82
+ });
83
+ }
84
+
85
+ /** Route chat completions to `chat`; answer availability probes with a model list. */
86
+ async function withFetch(chat: () => Promise<Response>, fn: () => Promise<void>): Promise<void> {
87
+ const original = globalThis.fetch;
88
+ globalThis.fetch = async (input: string | URL | Request) => {
89
+ const url = input instanceof Request ? input.url : String(input);
90
+ return url.includes("/chat/completions") ? chat() : modelsResponse();
91
+ };
92
+ resetLocalLlmRateLimitStateForTests();
93
+ try {
94
+ await fn();
95
+ } finally {
96
+ globalThis.fetch = original;
97
+ resetLocalLlmRateLimitStateForTests();
98
+ }
99
+ }
100
+
101
+ function failureStatus(diag: LocalLlmChatCompletionOptions["failureDiag"]): number | undefined {
102
+ const err = diag?.lastError;
103
+ return err instanceof Error && "status" in err && Number.isInteger(err.status) ? Number(err.status) : undefined;
104
+ }
105
+
106
+ // ---- pure helpers ----------------------------------------------------------
107
+
108
+ test("classifies 429/503/408 and LiteLLM cooldown bodies as rate limits, not 400s", () => {
109
+ assert.equal(isRateLimitedResponse(429, ""), true);
110
+ assert.equal(isRateLimitedResponse(503, ""), true);
111
+ assert.equal(isRateLimitedResponse(408, ""), true);
112
+ assert.equal(isRateLimitedResponse(500, "No deployments available for selected model"), true);
113
+ assert.equal(isRateLimitedResponse(500, "internal error"), false);
114
+ assert.equal(isRateLimitedResponse(400, "Context Window exceeded ... fallbacks cooldown_list"), false);
115
+ assert.equal(isRateLimitedResponse(502, ""), false);
116
+ });
117
+
118
+ test("parses Retry-After as delta-seconds or HTTP-date", () => {
119
+ assert.equal(parseRetryAfterMs("5"), 5_000);
120
+ assert.equal(parseRetryAfterMs(" 1.5 "), 1_500);
121
+ const now = Date.parse("2026-10-26T00:00:00Z");
122
+ assert.equal(parseRetryAfterMs("Mon, 26 Oct 2026 00:00:30 GMT", now), 30_000);
123
+ assert.equal(parseRetryAfterMs("Sun, 25 Oct 2026 23:00:00 GMT", now), 0);
124
+ assert.equal(parseRetryAfterMs("soon"), null);
125
+ assert.equal(parseRetryAfterMs(""), null);
126
+ assert.equal(parseRetryAfterMs(null), null);
127
+ assert.equal(parseRetryAfterMs("999999"), 10 * 60_000, "clamped to the sanity bound");
128
+ });
129
+
130
+ test("parses LiteLLM 'Try again in N seconds' body hint", () => {
131
+ assert.equal(parseTryAgainHintMs(LITELLM_429), 60_000);
132
+ assert.equal(parseTryAgainHintMs("try again in 250ms"), 250);
133
+ assert.equal(parseTryAgainHintMs("Try again in 2 minutes"), 120_000);
134
+ assert.equal(parseTryAgainHintMs("busy"), null);
135
+ });
136
+
137
+ test("full-jitter backoff grows exponentially and is capped", () => {
138
+ const max = () => 0.999999;
139
+ assert.equal(computeFullJitterBackoffMs(1, 100, 10_000, max), 99);
140
+ assert.equal(computeFullJitterBackoffMs(2, 100, 10_000, max), 199);
141
+ assert.equal(computeFullJitterBackoffMs(4, 100, 10_000, max), 799);
142
+ assert.equal(computeFullJitterBackoffMs(20, 100, 10_000, max), 9_999);
143
+ assert.equal(
144
+ computeFullJitterBackoffMs(3, 100, 10_000, () => 0),
145
+ 0
146
+ );
147
+ for (let i = 0; i < 200; i += 1) {
148
+ const v = computeFullJitterBackoffMs(3, 100, 10_000);
149
+ assert.ok(v >= 0 && v < 400);
150
+ }
151
+ });
152
+
153
+ test("server hint is a floor on the backoff delay", () => {
154
+ const settings = { backoffBaseMs: 100, backoffMaxMs: 1_000 };
155
+ assert.equal(
156
+ computeRateLimitDelayMs(1, settings, 5_000, () => 0.5),
157
+ 5_000
158
+ );
159
+ assert.equal(
160
+ computeRateLimitDelayMs(4, settings, 10, () => 0.5),
161
+ 400
162
+ );
163
+ assert.equal(
164
+ computeRateLimitDelayMs(4, settings, null, () => 0.5),
165
+ 400
166
+ );
167
+ });
168
+
169
+ test("settings default sanely and reject invalid values", () => {
170
+ assert.deepEqual(resolveRateLimitSettings({}), {
171
+ retryCount: 3,
172
+ backoffBaseMs: 1_000,
173
+ backoffMaxMs: 60_000,
174
+ maxConcurrency: 4,
175
+ });
176
+ const s = resolveRateLimitSettings({
177
+ localLlmRateLimitRetryCount: -1,
178
+ localLlmRateLimitBackoffBaseMs: 5_000,
179
+ localLlmRateLimitBackoffMaxMs: 10,
180
+ localLlmMaxConcurrency: 0,
181
+ });
182
+ assert.equal(s.retryCount, 3);
183
+ assert.equal(s.backoffMaxMs, 5_000, "max never below base");
184
+ assert.equal(s.maxConcurrency, 0);
185
+ });
186
+
187
+ test("limiter caps concurrency, serves high priority first, and drops aborted waiters", async () => {
188
+ const limiter = new LocalLlmConcurrencyLimiter(1);
189
+ const first = await limiter.acquire();
190
+ assert.ok(first);
191
+ const order: string[] = [];
192
+ const low = limiter.acquire().then((r) => {
193
+ order.push("low");
194
+ return r;
195
+ });
196
+ const aborter = new AbortController();
197
+ const aborted = limiter.acquire({ signal: aborter.signal });
198
+ const high = limiter.acquire({ highPriority: true }).then((r) => {
199
+ order.push("high");
200
+ return r;
201
+ });
202
+ assert.equal(limiter.queued, 3);
203
+ aborter.abort();
204
+ assert.equal(await aborted, null);
205
+ assert.equal(limiter.queued, 2);
206
+ first();
207
+ const highRelease = await high;
208
+ assert.deepEqual(order, ["high"]);
209
+ assert.equal(limiter.inFlight, 1);
210
+ highRelease?.();
211
+ (await low)?.();
212
+ assert.deepEqual(order, ["high", "low"]);
213
+ assert.equal(limiter.inFlight, 0);
214
+ });
215
+
216
+ // ---- client integration ----------------------------------------------------
217
+
218
+ test("429 is retried with backoff and then succeeds", async () => {
219
+ let calls = 0;
220
+ await withFetch(
221
+ async () => {
222
+ calls += 1;
223
+ return calls === 1 ? rateLimited(429) : ok("after backoff");
224
+ },
225
+ async () => {
226
+ const client = new LocalLlmClient(createConfig({ localLlmUrl: "http://127.0.0.1:1001" }));
227
+ const result = await client.chatCompletion([{ role: "user", content: "x" }], { operation: "lcm-summarize" });
228
+ assert.equal(result?.content, "after backoff");
229
+ assert.equal(calls, 2);
230
+ }
231
+ );
232
+ });
233
+
234
+ test("Retry-After header delays the retry", async () => {
235
+ const stamps: number[] = [];
236
+ await withFetch(
237
+ async () => {
238
+ stamps.push(Date.now());
239
+ return stamps.length === 1 ? rateLimited(503, { "retry-after": "1" }) : ok();
240
+ },
241
+ async () => {
242
+ const client = new LocalLlmClient(
243
+ createConfig({ localLlmUrl: "http://127.0.0.1:1002", localLlmRateLimitBackoffMaxMs: 5_000 })
244
+ );
245
+ const result = await client.chatCompletion([{ role: "user", content: "x" }]);
246
+ assert.equal(result?.content, "ok");
247
+ assert.equal(stamps.length, 2);
248
+ assert.ok(stamps[1] - stamps[0] >= 950, `waited ${stamps[1] - stamps[0]}ms, expected >= 1000ms`);
249
+ }
250
+ );
251
+ });
252
+
253
+ test("rate-limit retries are capped and report status 429", async () => {
254
+ let calls = 0;
255
+ await withFetch(
256
+ async () => {
257
+ calls += 1;
258
+ return rateLimited(429);
259
+ },
260
+ async () => {
261
+ const client = new LocalLlmClient(
262
+ createConfig({ localLlmUrl: "http://127.0.0.1:1003", localLlmRateLimitRetryCount: 2 })
263
+ );
264
+ const options: LocalLlmChatCompletionOptions = { failureDiag: {} };
265
+ const result = await client.chatCompletion([{ role: "user", content: "x" }], options);
266
+ assert.equal(result, null);
267
+ assert.equal(calls, 3, "one initial attempt + 2 rate-limit retries");
268
+ assert.equal(failureStatus(options.failureDiag), 429);
269
+ }
270
+ );
271
+ });
272
+
273
+ test("a server hint beyond the max skips the retry and gates every client on that endpoint", async () => {
274
+ let calls = 0;
275
+ await withFetch(
276
+ async () => {
277
+ calls += 1;
278
+ return rateLimited(429, {}, LITELLM_429);
279
+ },
280
+ async () => {
281
+ const url = "http://127.0.0.1:1004";
282
+ const a = new LocalLlmClient(createConfig({ localLlmUrl: url }));
283
+ const b = new LocalLlmClient(createConfig({ localLlmUrl: url }));
284
+ assert.equal(await a.chatCompletion([{ role: "user", content: "x" }]), null);
285
+ assert.equal(calls, 1, "60s hint > 20ms max: no immediate retry");
286
+ assert.ok(endpointBackoffRemainingMs(`${url}/v1/chat/completions`) > 50_000);
287
+ const options: LocalLlmChatCompletionOptions = { failureDiag: {} };
288
+ assert.equal(await b.chatCompletion([{ role: "user", content: "y" }], options), null);
289
+ assert.equal(calls, 1, "second client must not hit the proxy during the shared backoff");
290
+ assert.equal(failureStatus(options.failureDiag), 429);
291
+ }
292
+ );
293
+ });
294
+
295
+ test("503 rate-limit retries do not consume the 5xx retry budget", async () => {
296
+ let calls = 0;
297
+ await withFetch(
298
+ async () => {
299
+ calls += 1;
300
+ return calls < 3 ? rateLimited(503) : ok();
301
+ },
302
+ async () => {
303
+ const client = new LocalLlmClient(
304
+ createConfig({ localLlmUrl: "http://127.0.0.1:1005", localLlmRetry5xxCount: 0 })
305
+ );
306
+ assert.equal((await client.chatCompletion([{ role: "user", content: "x" }]))?.content, "ok");
307
+ assert.equal(calls, 3);
308
+ }
309
+ );
310
+ });
311
+
312
+ test("400 responses are not treated as rate limits", async () => {
313
+ let calls = 0;
314
+ await withFetch(
315
+ async () => {
316
+ calls += 1;
317
+ return new Response(JSON.stringify({ error: { message: "Context Window exceeded" } }), { status: 400 });
318
+ },
319
+ async () => {
320
+ const client = new LocalLlmClient(createConfig({ localLlmUrl: "http://127.0.0.1:1006" }));
321
+ assert.equal(await client.chatCompletion([{ role: "user", content: "x" }]), null);
322
+ assert.equal(calls, 1);
323
+ }
324
+ );
325
+ });
326
+
327
+ test("process-wide concurrency cap bounds in-flight requests across clients", async () => {
328
+ let inFlight = 0;
329
+ let peak = 0;
330
+ await withFetch(
331
+ async () => {
332
+ inFlight += 1;
333
+ peak = Math.max(peak, inFlight);
334
+ await new Promise((r) => setTimeout(r, 25));
335
+ inFlight -= 1;
336
+ return ok();
337
+ },
338
+ async () => {
339
+ const config = createConfig({ localLlmUrl: "http://127.0.0.1:1007", localLlmMaxConcurrency: 2 });
340
+ const clients = [new LocalLlmClient(config), new LocalLlmClient(config), new LocalLlmClient(config)];
341
+ const results = await Promise.all(
342
+ Array.from({ length: 7 }, (_, i) => clients[i % 3].chatCompletion([{ role: "user", content: `q${i}` }]))
343
+ );
344
+ assert.equal(results.filter((r) => r?.content === "ok").length, 7);
345
+ assert.equal(peak, 2);
346
+ }
347
+ );
348
+ });
349
+
350
+ test("rate-limit waits never exceed the caller's request timeout", async () => {
351
+ let calls = 0;
352
+ await withFetch(
353
+ async () => {
354
+ calls += 1;
355
+ return rateLimited(429, { "retry-after": "2" });
356
+ },
357
+ async () => {
358
+ const client = new LocalLlmClient(
359
+ createConfig({ localLlmUrl: "http://127.0.0.1:1008", localLlmRateLimitBackoffMaxMs: 5_000 })
360
+ );
361
+ const started = Date.now();
362
+ const options: LocalLlmChatCompletionOptions = { timeoutMs: 300, failureDiag: {} };
363
+ assert.equal(await client.chatCompletion([{ role: "user", content: "x" }], options), null);
364
+ assert.equal(await client.chatCompletion([{ role: "user", content: "y" }], { timeoutMs: 300 }), null);
365
+ assert.ok(Date.now() - started < 1_000, `took ${Date.now() - started}ms despite a 300ms budget`);
366
+ assert.equal(calls, 1, "a 2s hint beyond the 300ms budget is neither retried nor re-sent");
367
+ assert.equal(failureStatus(options.failureDiag), 429);
368
+ }
369
+ );
370
+ });
371
+
372
+ test("a queued attempt rechecks a cooldown recorded while it waited for a slot", async () => {
373
+ await withFetch(
374
+ async () => ok(),
375
+ async () => {
376
+ const endpoint = "http://127.0.0.1:1009/v1/chat/completions";
377
+ const settings = resolveRateLimitSettings(
378
+ createConfig({ localLlmMaxConcurrency: 1, localLlmRateLimitBackoffMaxMs: 1_000 })
379
+ );
380
+ const holder = await new LocalLlmRateLimitSession(endpoint, settings, false).beforeAttempt();
381
+ assert.equal(holder.kind, "go");
382
+ const queued = new LocalLlmRateLimitSession(endpoint, settings, false, 5_000).beforeAttempt();
383
+ extendEndpointBackoff(endpoint, 60_000);
384
+ if (holder.kind === "go") holder.release();
385
+ const gate = await queued;
386
+ assert.equal(gate.kind, "skip");
387
+ assert.equal(gate.kind === "skip" ? gate.reason : null, "backoff");
388
+ }
389
+ );
390
+ });
391
+
392
+ test("waiting for a concurrency slot is bounded by the wait budget", async () => {
393
+ await withFetch(
394
+ async () => ok(),
395
+ async () => {
396
+ const endpoint = "http://127.0.0.1:1010/v1/chat/completions";
397
+ const settings = resolveRateLimitSettings(createConfig({ localLlmMaxConcurrency: 1 }));
398
+ const holder = await new LocalLlmRateLimitSession(endpoint, settings, false).beforeAttempt();
399
+ const started = Date.now();
400
+ const gate = await new LocalLlmRateLimitSession(endpoint, settings, false, 50).beforeAttempt();
401
+ assert.equal(gate.kind === "skip" ? gate.reason : gate.kind, "queue");
402
+ assert.ok(Date.now() - started < 1_000);
403
+ if (holder.kind === "go") holder.release();
404
+ }
405
+ );
406
+ });