@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
@@ -0,0 +1,418 @@
1
+ /**
2
+ * Process-wide rate-limit handling for the local LLM client (COS-447).
3
+ *
4
+ * A LiteLLM proxy in front of the local models answers `429 No deployments
5
+ * available` (and `503`/`408`) when every deployment is busy or in cooldown.
6
+ * Before this module the client returned on the first 429 and the caller
7
+ * loop immediately issued the next request, so one daemon could hammer the
8
+ * proxy with thousands of calls per hour while it was saturated.
9
+ *
10
+ * This module provides three pieces that the client composes:
11
+ *
12
+ * - classification of rate-limit responses and of the retry hint
13
+ * (`Retry-After` header as delta-seconds or HTTP-date, falling back to
14
+ * LiteLLM's `Try again in N seconds` body text);
15
+ * - exponential backoff with full jitter;
16
+ * - process-wide state shared by every `LocalLlmClient` instance: a
17
+ * concurrency semaphore for in-flight HTTP attempts and a per-endpoint
18
+ * "do not call before" deadline, so one client's 429 slows every client
19
+ * pointed at the same endpoint.
20
+ *
21
+ * State lives on `globalThis` so duplicated module copies in bundled output
22
+ * still share one limiter.
23
+ */
24
+
25
+ export const RATE_LIMIT_STATUSES: ReadonlySet<number> = new Set([408, 429, 503]);
26
+
27
+ const RATE_LIMIT_BODY_PATTERN = /no deployments available|cooldown_list|all deployments.*cooldown/i;
28
+ const TRY_AGAIN_PATTERN = /try again in (\d+(?:\.\d+)?) ?(seconds?|secs?|s|milliseconds?|ms|minutes?|mins?)\b/i;
29
+
30
+ /** Upper bound on any server-provided wait we will record (sanity clamp). */
31
+ export const MAX_RECORDED_BACKOFF_MS = 10 * 60_000;
32
+
33
+ export const DEFAULT_RATE_LIMIT_RETRY_COUNT = 3;
34
+ export const DEFAULT_RATE_LIMIT_BACKOFF_BASE_MS = 1_000;
35
+ export const DEFAULT_RATE_LIMIT_BACKOFF_MAX_MS = 60_000;
36
+ export const DEFAULT_MAX_CONCURRENCY = 4;
37
+
38
+ export interface LocalLlmRateLimitSettings {
39
+ retryCount: number;
40
+ backoffBaseMs: number;
41
+ backoffMaxMs: number;
42
+ /** 0 disables the process-wide concurrency cap. */
43
+ maxConcurrency: number;
44
+ }
45
+
46
+ function nonNegativeInt(value: number | undefined, fallback: number): number {
47
+ if (value === undefined || !Number.isFinite(value) || value < 0) return fallback;
48
+ return Math.floor(value);
49
+ }
50
+
51
+ export function resolveRateLimitSettings(config: {
52
+ localLlmRateLimitRetryCount?: number;
53
+ localLlmRateLimitBackoffBaseMs?: number;
54
+ localLlmRateLimitBackoffMaxMs?: number;
55
+ localLlmMaxConcurrency?: number;
56
+ }): LocalLlmRateLimitSettings {
57
+ const backoffBaseMs = Math.max(
58
+ 1,
59
+ nonNegativeInt(config.localLlmRateLimitBackoffBaseMs, DEFAULT_RATE_LIMIT_BACKOFF_BASE_MS)
60
+ );
61
+ const backoffMaxMs = Math.max(
62
+ backoffBaseMs,
63
+ nonNegativeInt(config.localLlmRateLimitBackoffMaxMs, DEFAULT_RATE_LIMIT_BACKOFF_MAX_MS)
64
+ );
65
+ return {
66
+ retryCount: nonNegativeInt(config.localLlmRateLimitRetryCount, DEFAULT_RATE_LIMIT_RETRY_COUNT),
67
+ backoffBaseMs,
68
+ backoffMaxMs,
69
+ maxConcurrency: nonNegativeInt(config.localLlmMaxConcurrency, DEFAULT_MAX_CONCURRENCY),
70
+ };
71
+ }
72
+
73
+ /**
74
+ * True for responses that mean "the endpoint is saturated, slow down":
75
+ * 408/429/503, or a 5xx whose body carries LiteLLM's cooldown wording.
76
+ * 400s are never rate limits (context-window errors mention fallbacks).
77
+ */
78
+ export function isRateLimitedResponse(status: number, bodyText: string): boolean {
79
+ if (RATE_LIMIT_STATUSES.has(status)) return true;
80
+ return status >= 500 && RATE_LIMIT_BODY_PATTERN.test(bodyText);
81
+ }
82
+
83
+ /**
84
+ * Parse an HTTP `Retry-After` value (RFC 9110: delta-seconds or HTTP-date).
85
+ * Returns milliseconds to wait, or null when absent/invalid.
86
+ */
87
+ export function parseRetryAfterMs(value: string | null | undefined, nowMs: number = Date.now()): number | null {
88
+ if (value === null || value === undefined) return null;
89
+ const trimmed = value.trim();
90
+ if (!trimmed) return null;
91
+ if (/^\d+(\.\d+)?$/.test(trimmed)) {
92
+ return Math.min(MAX_RECORDED_BACKOFF_MS, Math.round(Number(trimmed) * 1000));
93
+ }
94
+ const dateMs = Date.parse(trimmed);
95
+ if (Number.isNaN(dateMs)) return null;
96
+ return Math.min(MAX_RECORDED_BACKOFF_MS, Math.max(0, dateMs - nowMs));
97
+ }
98
+
99
+ /** Parse LiteLLM's `Try again in N seconds` hint from an error body. */
100
+ export function parseTryAgainHintMs(bodyText: string): number | null {
101
+ const match = bodyText.match(TRY_AGAIN_PATTERN);
102
+ if (!match) return null;
103
+ const amount = Number(match[1]);
104
+ if (!Number.isFinite(amount)) return null;
105
+ const unit = match[2].toLowerCase();
106
+ let ms: number;
107
+ if (unit.startsWith("ms") || unit.startsWith("milli")) ms = amount;
108
+ else if (unit.startsWith("min")) ms = amount * 60_000;
109
+ else ms = amount * 1000;
110
+ return Math.min(MAX_RECORDED_BACKOFF_MS, Math.round(ms));
111
+ }
112
+
113
+ /** Server-provided wait: `Retry-After` header first, then the body hint. */
114
+ export function serverRetryHintMs(
115
+ retryAfterHeader: string | null | undefined,
116
+ bodyText: string,
117
+ nowMs: number = Date.now()
118
+ ): number | null {
119
+ return parseRetryAfterMs(retryAfterHeader, nowMs) ?? parseTryAgainHintMs(bodyText);
120
+ }
121
+
122
+ /**
123
+ * Exponential backoff with full jitter: uniform in [0, min(max, base * 2^(n-1))].
124
+ * `attempt` is 1-based (the number of consecutive rate-limit responses).
125
+ */
126
+ export function computeFullJitterBackoffMs(
127
+ attempt: number,
128
+ baseMs: number,
129
+ maxMs: number,
130
+ random: () => number = Math.random
131
+ ): number {
132
+ const exponent = Math.max(0, Math.min(30, attempt - 1));
133
+ const ceiling = Math.min(maxMs, baseMs * 2 ** exponent);
134
+ return Math.floor(random() * ceiling);
135
+ }
136
+
137
+ /**
138
+ * Delay before the next attempt after a rate-limit response. A server hint
139
+ * is a floor (never call earlier than asked); jitter is added on top of the
140
+ * exponential term so synchronized callers spread out.
141
+ */
142
+ export function computeRateLimitDelayMs(
143
+ attempt: number,
144
+ settings: Pick<LocalLlmRateLimitSettings, "backoffBaseMs" | "backoffMaxMs">,
145
+ serverHintMs: number | null,
146
+ random: () => number = Math.random
147
+ ): number {
148
+ const jittered = computeFullJitterBackoffMs(attempt, settings.backoffBaseMs, settings.backoffMaxMs, random);
149
+ return serverHintMs === null ? jittered : Math.max(serverHintMs, jittered);
150
+ }
151
+
152
+ type Waiter = { highPriority: boolean; grant: () => void };
153
+
154
+ /** FIFO counting semaphore with a high-priority lane (recall-critical first). */
155
+ export class LocalLlmConcurrencyLimiter {
156
+ private active = 0;
157
+ private max: number;
158
+ private readonly waiters: Waiter[] = [];
159
+
160
+ constructor(max: number) {
161
+ this.max = max;
162
+ }
163
+
164
+ setMax(max: number): void {
165
+ this.max = max;
166
+ this.pump();
167
+ }
168
+
169
+ get inFlight(): number {
170
+ return this.active;
171
+ }
172
+
173
+ get queued(): number {
174
+ return this.waiters.length;
175
+ }
176
+
177
+ /**
178
+ * Wait for a slot. Resolves to a release function, or null when the
179
+ * signal aborts first. `max <= 0` means unlimited.
180
+ */
181
+ acquire(
182
+ options: { highPriority?: boolean; signal?: AbortSignal; timeoutMs?: number } = {}
183
+ ): Promise<(() => void) | null> {
184
+ const { signal, timeoutMs } = options;
185
+ if (signal?.aborted) return Promise.resolve(null);
186
+ if (this.max <= 0 || (this.active < this.max && this.waiters.length === 0)) {
187
+ this.active += 1;
188
+ return Promise.resolve(this.releaser());
189
+ }
190
+ if (timeoutMs !== undefined && timeoutMs <= 0) return Promise.resolve(null);
191
+ return new Promise((resolve) => {
192
+ let timer: NodeJS.Timeout | undefined;
193
+ const waiter: Waiter = {
194
+ highPriority: options.highPriority === true,
195
+ grant: () => {
196
+ signal?.removeEventListener("abort", onAbort);
197
+ if (timer) clearTimeout(timer);
198
+ resolve(this.releaser());
199
+ },
200
+ };
201
+ const onAbort = (): void => {
202
+ const index = this.waiters.indexOf(waiter);
203
+ if (index >= 0) this.waiters.splice(index, 1);
204
+ signal?.removeEventListener("abort", onAbort);
205
+ if (timer) clearTimeout(timer);
206
+ resolve(null);
207
+ };
208
+ if (waiter.highPriority) {
209
+ const firstLow = this.waiters.findIndex((w) => !w.highPriority);
210
+ if (firstLow < 0) this.waiters.push(waiter);
211
+ else this.waiters.splice(firstLow, 0, waiter);
212
+ } else {
213
+ this.waiters.push(waiter);
214
+ }
215
+ signal?.addEventListener("abort", onAbort, { once: true });
216
+ if (timeoutMs !== undefined) timer = setTimeout(onAbort, timeoutMs);
217
+ });
218
+ }
219
+
220
+ private releaser(): () => void {
221
+ let released = false;
222
+ return () => {
223
+ if (released) return;
224
+ released = true;
225
+ this.active -= 1;
226
+ this.pump();
227
+ };
228
+ }
229
+
230
+ private pump(): void {
231
+ while (this.waiters.length > 0 && (this.max <= 0 || this.active < this.max)) {
232
+ const next = this.waiters.shift();
233
+ if (!next) break;
234
+ this.active += 1;
235
+ next.grant();
236
+ }
237
+ }
238
+ }
239
+
240
+ interface EndpointBackoffState {
241
+ untilMs: number;
242
+ consecutiveRateLimits: number;
243
+ }
244
+
245
+ interface RateLimitGlobalState {
246
+ limiter: LocalLlmConcurrencyLimiter;
247
+ endpoints: Map<string, EndpointBackoffState>;
248
+ }
249
+
250
+ declare global {
251
+ // Shared across duplicated module copies in bundled output.
252
+ var __remnicLocalLlmRateLimitState: RateLimitGlobalState | undefined;
253
+ }
254
+
255
+ function globalState(): RateLimitGlobalState {
256
+ globalThis.__remnicLocalLlmRateLimitState ??= {
257
+ limiter: new LocalLlmConcurrencyLimiter(DEFAULT_MAX_CONCURRENCY),
258
+ endpoints: new Map(),
259
+ };
260
+ return globalThis.__remnicLocalLlmRateLimitState;
261
+ }
262
+
263
+ /** Process-wide limiter, resized to the caller's configured cap. */
264
+ export function getLocalLlmConcurrencyLimiter(maxConcurrency: number): LocalLlmConcurrencyLimiter {
265
+ const { limiter } = globalState();
266
+ limiter.setMax(maxConcurrency);
267
+ return limiter;
268
+ }
269
+
270
+ /** Milliseconds until `endpoint` may be called again (0 when clear). */
271
+ export function endpointBackoffRemainingMs(endpoint: string, nowMs: number = Date.now()): number {
272
+ const entry = globalState().endpoints.get(endpoint);
273
+ return entry ? Math.max(0, entry.untilMs - nowMs) : 0;
274
+ }
275
+
276
+ /**
277
+ * Record a rate-limit response for `endpoint`. Returns the consecutive
278
+ * rate-limit count (1-based) to feed the backoff exponent.
279
+ */
280
+ export function noteEndpointRateLimited(endpoint: string): number {
281
+ const { endpoints } = globalState();
282
+ const entry = endpoints.get(endpoint) ?? { untilMs: 0, consecutiveRateLimits: 0 };
283
+ entry.consecutiveRateLimits += 1;
284
+ endpoints.set(endpoint, entry);
285
+ return entry.consecutiveRateLimits;
286
+ }
287
+
288
+ /** Push the endpoint's "do not call before" deadline out to at least `nowMs + delayMs`. */
289
+ export function extendEndpointBackoff(endpoint: string, delayMs: number, nowMs: number = Date.now()): void {
290
+ const { endpoints } = globalState();
291
+ const entry = endpoints.get(endpoint) ?? { untilMs: 0, consecutiveRateLimits: 0 };
292
+ const bounded = Math.min(MAX_RECORDED_BACKOFF_MS, Math.max(0, delayMs));
293
+ entry.untilMs = Math.max(entry.untilMs, nowMs + bounded);
294
+ endpoints.set(endpoint, entry);
295
+ }
296
+
297
+ /**
298
+ * Reset the backoff exponent after a non-rate-limited response. An already
299
+ * recorded deadline is left to expire so concurrent callers that saw the
300
+ * 429 still wait it out.
301
+ */
302
+ export function noteEndpointRecovered(endpoint: string): void {
303
+ const entry = globalState().endpoints.get(endpoint);
304
+ if (entry) entry.consecutiveRateLimits = 0;
305
+ }
306
+
307
+ /** Test seam: drop all process-wide state. */
308
+ export function resetLocalLlmRateLimitStateForTests(): void {
309
+ const state = globalState();
310
+ state.endpoints.clear();
311
+ state.limiter = new LocalLlmConcurrencyLimiter(DEFAULT_MAX_CONCURRENCY);
312
+ }
313
+
314
+ export type RateLimitAttemptGate =
315
+ | { kind: "go"; release: () => void }
316
+ | { kind: "aborted" }
317
+ | { kind: "skip"; reason: "backoff" | "queue"; remainingMs: number };
318
+
319
+ export type RateLimitDecision =
320
+ | { kind: "not-rate-limited" }
321
+ | { kind: "retry"; delayMs: number; hintMs: number | null; consecutive: number; retry: number }
322
+ | { kind: "give-up"; delayMs: number; retries: number };
323
+
324
+ /**
325
+ * Per-request driver for the rate-limit policy. The client calls
326
+ * `beforeAttempt` ahead of every HTTP attempt (waits out the shared endpoint
327
+ * deadline, then takes a concurrency slot) and `onResponse` after every
328
+ * non-ok response.
329
+ */
330
+ export class LocalLlmRateLimitSession {
331
+ private retries = 0;
332
+ private readonly limiter: LocalLlmConcurrencyLimiter;
333
+ readonly settings: LocalLlmRateLimitSettings;
334
+
335
+ /**
336
+ * @param waitBudgetMs the caller's per-attempt time budget. Waiting for the
337
+ * endpoint backoff or a concurrency slot never exceeds it, and a backoff
338
+ * longer than it skips the request instead (fail open to the caller).
339
+ */
340
+ constructor(
341
+ private readonly endpoint: string,
342
+ settings: LocalLlmRateLimitSettings,
343
+ private readonly highPriority: boolean,
344
+ private readonly waitBudgetMs: number = settings.backoffMaxMs
345
+ ) {
346
+ this.settings = settings;
347
+ this.limiter = getLocalLlmConcurrencyLimiter(settings.maxConcurrency);
348
+ }
349
+
350
+ get retryCount(): number {
351
+ return this.retries;
352
+ }
353
+
354
+ async beforeAttempt(signal?: AbortSignal): Promise<RateLimitAttemptGate> {
355
+ const deadlineMs = Date.now() + Math.max(0, this.waitBudgetMs);
356
+ // Recheck the shared deadline after every async wait: another attempt may
357
+ // have recorded or extended a cooldown while this one slept or queued.
358
+ for (;;) {
359
+ const remainingMs = endpointBackoffRemainingMs(this.endpoint);
360
+ if (remainingMs > 0) {
361
+ if (remainingMs > Math.min(this.settings.backoffMaxMs, deadlineMs - Date.now())) {
362
+ return { kind: "skip", reason: "backoff", remainingMs };
363
+ }
364
+ if (!(await waitWithSignal(remainingMs, signal))) return { kind: "aborted" };
365
+ continue;
366
+ }
367
+ const release = await this.limiter.acquire({
368
+ highPriority: this.highPriority,
369
+ signal,
370
+ timeoutMs: deadlineMs - Date.now(),
371
+ });
372
+ if (!release) return signal?.aborted ? { kind: "aborted" } : { kind: "skip", reason: "queue", remainingMs: 0 };
373
+ if (endpointBackoffRemainingMs(this.endpoint) > 0) {
374
+ release();
375
+ continue;
376
+ }
377
+ return { kind: "go", release };
378
+ }
379
+ }
380
+
381
+ onSuccess(): void {
382
+ noteEndpointRecovered(this.endpoint);
383
+ }
384
+
385
+ onResponse(status: number, retryAfterHeader: string | null | undefined, bodyText: string): RateLimitDecision {
386
+ if (!isRateLimitedResponse(status, bodyText)) {
387
+ noteEndpointRecovered(this.endpoint);
388
+ return { kind: "not-rate-limited" };
389
+ }
390
+ const consecutive = noteEndpointRateLimited(this.endpoint);
391
+ const hintMs = serverRetryHintMs(retryAfterHeader, bodyText);
392
+ const delayMs = computeRateLimitDelayMs(consecutive, this.settings, hintMs);
393
+ extendEndpointBackoff(this.endpoint, delayMs);
394
+ if (this.retries >= this.settings.retryCount || delayMs > Math.min(this.settings.backoffMaxMs, this.waitBudgetMs)) {
395
+ return { kind: "give-up", delayMs, retries: this.retries };
396
+ }
397
+ this.retries += 1;
398
+ return { kind: "retry", delayMs, hintMs, consecutive, retry: this.retries };
399
+ }
400
+ }
401
+
402
+ function waitWithSignal(ms: number, signal?: AbortSignal): Promise<boolean> {
403
+ return new Promise((resolve) => {
404
+ if (signal?.aborted) {
405
+ resolve(false);
406
+ return;
407
+ }
408
+ const onAbort = (): void => {
409
+ clearTimeout(timer);
410
+ resolve(false);
411
+ };
412
+ const timer = setTimeout(() => {
413
+ signal?.removeEventListener("abort", onAbort);
414
+ resolve(true);
415
+ }, ms);
416
+ signal?.addEventListener("abort", onAbort, { once: true });
417
+ });
418
+ }
package/src/local-llm.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { log } from "./logger.js";
2
+ import { isUnsupportedJsonSchemaError } from "./fallback-llm-codex-subscription.js";
2
3
  import { estimateTokenCount } from "./token-estimate.js";
3
4
  import type { PluginConfig } from "./types.js";
4
5
  import fs from "node:fs";
@@ -9,6 +10,7 @@ import { mergeEnv, readEnvVar } from "./runtime/env.js";
9
10
  import { resolveLocalLlmCapabilities } from "./capabilities.js";
10
11
  import { resolvePipelineProcessingCapabilities } from "./capabilities.js";
11
12
  import { ChatTransport } from "./local-llm-transport.js";
13
+ import { LocalLlmRateLimitSession, resolveRateLimitSettings } from "./local-llm-rate-limit.js";
12
14
  import {
13
15
  extractNonRecoverableBackendReason,
14
16
  extractNonRecoverableBackendReasonFromErrorText,
@@ -952,7 +954,7 @@ export class LocalLlmClient {
952
954
  ? `${baseUrl}/chat/completions`
953
955
  : `${baseUrl}/v1/chat/completions`;
954
956
 
955
- const requestBodyJson = JSON.stringify(requestBody);
957
+ let requestBodyJson = JSON.stringify(requestBody);
956
958
  log.debug(
957
959
  `local LLM: sending request to ${chatUrl} with model ${this.config.localLlmModel}`,
958
960
  );
@@ -964,11 +966,30 @@ export class LocalLlmClient {
964
966
  ? Math.min(this.config.localLlmTimeoutMs, options.timeoutMs)
965
967
  : this.config.localLlmTimeoutMs;
966
968
  const maxAttempts = 1 + Math.max(0, this.config.localLlmRetry5xxCount);
969
+ // COS-447: rate-limit responses (429/503/408, LiteLLM cooldown) get
970
+ // their own retry budget with exponential full-jitter backoff, and a
971
+ // process-wide semaphore caps concurrent HTTP attempts across clients.
972
+ // Waits are bounded by the request timeout so a short caller budget
973
+ // (e.g. rerankTimeoutMs) is never stretched by a long cooldown.
974
+ const rateLimit = new LocalLlmRateLimitSession(chatUrl, resolveRateLimitSettings(this.config),
975
+ (queueMeta?.priority ?? options.priority) === "recall-critical", effectiveTimeoutMs);
976
+ let lastRateLimitDelayMs: number | null = null;
967
977
  let response: Response | null = null, responseBody = "";
968
978
  let lastAbortError: Error | null = null;
969
979
  for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
970
980
  response = null;
971
981
  responseBody = "";
982
+ // Wait out the shared endpoint backoff, then take a concurrency slot.
983
+ const gate = await rateLimit.beforeAttempt(options.signal);
984
+ if (gate.kind === "skip") {
985
+ log.warn(gate.reason === "queue"
986
+ ? `local LLM concurrency queue exceeded the ${effectiveTimeoutMs}ms request budget; skipping request (op=${operation})`
987
+ : `local LLM rate-limit backoff active (${gate.remainingMs}ms remaining); skipping request (op=${operation})`);
988
+ if (options.failureDiag)
989
+ options.failureDiag.lastError = Object.assign(new Error("local LLM rate limited"), { status: 429 });
990
+ return null;
991
+ }
992
+ if (gate.kind === "aborted") return null;
972
993
  const attemptAbort = new AbortController();
973
994
  const onCallerAbort = (): void => {
974
995
  attemptAbort.abort(options.signal?.reason);
@@ -984,14 +1005,31 @@ export class LocalLlmClient {
984
1005
  try {
985
1006
  response = await this.chatTransport.post({
986
1007
  url: chatUrl,
987
- headers: this.buildRequestHeaders({
988
- "Content-Type": "application/json",
989
- }),
1008
+ headers: this.buildRequestHeaders({ "Content-Type": "application/json" }),
990
1009
  body: requestBodyJson,
991
1010
  signal: attemptAbort.signal,
992
1011
  budgetMs: this.config.localLlmTimeoutMs,
993
1012
  });
994
1013
  responseBody = await response.text();
1014
+ if (
1015
+ response.status === 400 &&
1016
+ requestBody.response_format &&
1017
+ isUnsupportedJsonSchemaError(`400 ${responseBody}`)
1018
+ ) {
1019
+ // Match gateway degradation without extending this attempt's deadline.
1020
+ delete requestBody.response_format;
1021
+ requestBodyJson = JSON.stringify(requestBody);
1022
+ log.debug("local LLM rejected native JSON schema; retrying without it");
1023
+ response = null;
1024
+ response = await this.chatTransport.post({
1025
+ url: chatUrl,
1026
+ headers: this.buildRequestHeaders({ "Content-Type": "application/json" }),
1027
+ body: requestBodyJson,
1028
+ signal: attemptAbort.signal,
1029
+ budgetMs: this.config.localLlmTimeoutMs,
1030
+ });
1031
+ responseBody = await response.text();
1032
+ }
995
1033
  } catch (err) {
996
1034
  const error = err instanceof Error ? err : new Error(String(err));
997
1035
  if (options.signal?.aborted) {
@@ -1016,9 +1054,13 @@ export class LocalLlmClient {
1016
1054
  } finally {
1017
1055
  clearTimeout(attemptTimeout);
1018
1056
  options.signal?.removeEventListener("abort", onCallerAbort);
1057
+ gate.release();
1019
1058
  }
1020
1059
 
1021
- if (response.ok) break;
1060
+ if (response.ok) {
1061
+ rateLimit.onSuccess();
1062
+ break;
1063
+ }
1022
1064
  if (response.status >= 500 && attempt < maxAttempts) {
1023
1065
  const nonRecoverableReason =
1024
1066
  extractNonRecoverableBackendReasonFromErrorText(responseBody);
@@ -1031,6 +1073,14 @@ export class LocalLlmClient {
1031
1073
  return null;
1032
1074
  }
1033
1075
  }
1076
+ const decision = rateLimit.onResponse(response.status, response.headers?.get("retry-after"), responseBody);
1077
+ lastRateLimitDelayMs = decision.kind === "not-rate-limited" ? null : decision.delayMs;
1078
+ if (decision.kind === "give-up") break;
1079
+ if (decision.kind === "retry") {
1080
+ log.warn(`local LLM rate limited: ${response.status}; backing off ${decision.delayMs}ms before retry ${decision.retry}/${rateLimit.settings.retryCount} (op=${operation}, serverHintMs=${decision.hintMs ?? "none"}, consecutive=${decision.consecutive})`);
1081
+ attempt -= 1; // own retry budget; the loop top waits out the backoff
1082
+ continue;
1083
+ }
1034
1084
  if (response.status < 500 || attempt >= maxAttempts) break;
1035
1085
 
1036
1086
  const backoffMs = this.config.localLlmRetryBackoffMs * attempt;
@@ -1056,6 +1106,8 @@ export class LocalLlmClient {
1056
1106
  }
1057
1107
 
1058
1108
  if (!response.ok) {
1109
+ if (lastRateLimitDelayMs !== null)
1110
+ log.warn(`local LLM rate-limit retries exhausted: op=${operation} retries=${rateLimit.retryCount} nextBackoffMs=${lastRateLimitDelayMs}`);
1059
1111
  let reason = "";
1060
1112
  try {
1061
1113
  const parsed = JSON.parse(responseBody) as { error?: { message?: string } };
package/src/types.ts CHANGED
@@ -1648,6 +1648,14 @@ export interface PluginConfig
1648
1648
  localLlmRetryBackoffMs: number;
1649
1649
  localLlm400TripThreshold: number;
1650
1650
  localLlm400CooldownMs: number;
1651
+ /** COS-447: retries for rate-limit responses (429/503/408, LiteLLM cooldown). Default 3. */
1652
+ localLlmRateLimitRetryCount?: number;
1653
+ /** COS-447: base delay for exponential full-jitter rate-limit backoff (ms). Default 1000. */
1654
+ localLlmRateLimitBackoffBaseMs?: number;
1655
+ /** COS-447: ceiling for rate-limit backoff; longer server hints skip the retry (ms). Default 60000. */
1656
+ localLlmRateLimitBackoffMaxMs?: number;
1657
+ /** COS-447: process-wide cap on concurrent local LLM HTTP attempts (0 = unlimited). Default 4. */
1658
+ localLlmMaxConcurrency?: number;
1651
1659
  /** Ollama-backend thinking suppression: `reasoning_effort` value injected on
1652
1660
  * `/v1/chat/completions` for THINKING_SUPPRESSED_OPERATIONS ("none" | "low" |
1653
1661
  * "medium" | "high" | "max"; "" disables injection). Ollama ignores