@st-gr/sail-proxy 0.9.2 → 0.9.4

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 (244) hide show
  1. package/bundled/gateway/node_modules/.package-lock.json +142 -93
  2. package/bundled/gateway/node_modules/axios/CHANGELOG.md +0 -42
  3. package/bundled/gateway/node_modules/axios/README.md +237 -237
  4. package/bundled/gateway/node_modules/axios/dist/axios.js +21 -113
  5. package/bundled/gateway/node_modules/axios/dist/axios.js.map +1 -0
  6. package/bundled/gateway/node_modules/axios/dist/axios.min.js +2 -2
  7. package/bundled/gateway/node_modules/axios/dist/axios.min.js.map +1 -1
  8. package/bundled/gateway/node_modules/axios/dist/browser/axios.cjs +15 -121
  9. package/bundled/gateway/node_modules/axios/dist/browser/axios.cjs.map +1 -0
  10. package/bundled/gateway/node_modules/axios/dist/esm/axios.js +15 -121
  11. package/bundled/gateway/node_modules/axios/dist/esm/axios.js.map +1 -0
  12. package/bundled/gateway/node_modules/axios/dist/esm/axios.min.js +2 -2
  13. package/bundled/gateway/node_modules/axios/dist/esm/axios.min.js.map +1 -1
  14. package/bundled/gateway/node_modules/axios/dist/node/axios.cjs +107 -249
  15. package/bundled/gateway/node_modules/axios/dist/node/axios.cjs.map +1 -0
  16. package/bundled/gateway/node_modules/axios/index.d.cts +3 -8
  17. package/bundled/gateway/node_modules/axios/index.d.ts +1 -4
  18. package/bundled/gateway/node_modules/axios/lib/adapters/fetch.js +0 -79
  19. package/bundled/gateway/node_modules/axios/lib/adapters/http.js +118 -62
  20. package/bundled/gateway/node_modules/axios/lib/core/Axios.js +0 -1
  21. package/bundled/gateway/node_modules/axios/lib/core/AxiosHeaders.js +2 -2
  22. package/bundled/gateway/node_modules/axios/lib/defaults/transitional.js +0 -1
  23. package/bundled/gateway/node_modules/axios/lib/env/data.js +1 -1
  24. package/bundled/gateway/node_modules/axios/lib/helpers/buildURL.js +1 -1
  25. package/bundled/gateway/node_modules/axios/lib/helpers/formDataToStream.js +2 -2
  26. package/bundled/gateway/node_modules/axios/lib/helpers/resolveConfig.js +6 -12
  27. package/bundled/gateway/node_modules/axios/lib/helpers/toFormData.js +1 -1
  28. package/bundled/gateway/node_modules/axios/lib/utils.js +2 -23
  29. package/bundled/gateway/node_modules/axios/package.json +13 -29
  30. package/bundled/gateway/node_modules/body-parser/HISTORY.md +0 -14
  31. package/bundled/gateway/node_modules/body-parser/SECURITY.md +25 -0
  32. package/bundled/gateway/node_modules/body-parser/lib/types/json.js +5 -1
  33. package/bundled/gateway/node_modules/body-parser/lib/types/urlencoded.js +15 -7
  34. package/bundled/gateway/node_modules/body-parser/node_modules/qs/.editorconfig +46 -0
  35. package/bundled/gateway/node_modules/body-parser/node_modules/qs/.eslintrc +38 -0
  36. package/bundled/gateway/node_modules/body-parser/node_modules/qs/.github/FUNDING.yml +12 -0
  37. package/bundled/gateway/node_modules/body-parser/node_modules/qs/.nycrc +13 -0
  38. package/bundled/gateway/node_modules/body-parser/node_modules/qs/CHANGELOG.md +600 -0
  39. package/bundled/gateway/node_modules/body-parser/node_modules/qs/LICENSE.md +29 -0
  40. package/bundled/gateway/node_modules/body-parser/node_modules/qs/README.md +709 -0
  41. package/bundled/gateway/node_modules/body-parser/node_modules/qs/dist/qs.js +90 -0
  42. package/bundled/gateway/node_modules/body-parser/node_modules/qs/lib/formats.js +23 -0
  43. package/bundled/gateway/node_modules/body-parser/node_modules/qs/lib/index.js +11 -0
  44. package/bundled/gateway/node_modules/body-parser/node_modules/qs/lib/parse.js +296 -0
  45. package/bundled/gateway/node_modules/body-parser/node_modules/qs/lib/stringify.js +351 -0
  46. package/bundled/gateway/node_modules/body-parser/node_modules/qs/lib/utils.js +265 -0
  47. package/bundled/gateway/node_modules/body-parser/node_modules/qs/package.json +91 -0
  48. package/bundled/gateway/node_modules/body-parser/node_modules/qs/test/empty-keys-cases.js +267 -0
  49. package/bundled/gateway/node_modules/body-parser/node_modules/qs/test/parse.js +1170 -0
  50. package/bundled/gateway/node_modules/body-parser/node_modules/qs/test/stringify.js +1298 -0
  51. package/bundled/gateway/node_modules/body-parser/node_modules/qs/test/utils.js +136 -0
  52. package/bundled/gateway/node_modules/body-parser/package.json +10 -9
  53. package/bundled/gateway/node_modules/cookie/index.js +35 -24
  54. package/bundled/gateway/node_modules/cookie/package.json +1 -1
  55. package/bundled/gateway/node_modules/cookie-signature/History.md +1 -5
  56. package/bundled/gateway/node_modules/cookie-signature/index.js +6 -6
  57. package/bundled/gateway/node_modules/cookie-signature/package.json +2 -2
  58. package/bundled/gateway/node_modules/express/History.md +0 -43
  59. package/bundled/gateway/node_modules/express/lib/response.js +0 -1
  60. package/bundled/gateway/node_modules/express/lib/utils.js +1 -2
  61. package/bundled/gateway/node_modules/express/node_modules/qs/.editorconfig +43 -0
  62. package/bundled/gateway/node_modules/express/node_modules/qs/.eslintrc +38 -0
  63. package/bundled/gateway/node_modules/express/node_modules/qs/.github/FUNDING.yml +12 -0
  64. package/bundled/gateway/node_modules/express/node_modules/qs/.nycrc +13 -0
  65. package/bundled/gateway/node_modules/express/node_modules/qs/CHANGELOG.md +546 -0
  66. package/bundled/gateway/node_modules/express/node_modules/qs/LICENSE.md +29 -0
  67. package/bundled/gateway/node_modules/express/node_modules/qs/README.md +625 -0
  68. package/bundled/gateway/node_modules/express/node_modules/qs/dist/qs.js +2054 -0
  69. package/bundled/gateway/node_modules/express/node_modules/qs/lib/formats.js +23 -0
  70. package/bundled/gateway/node_modules/express/node_modules/qs/lib/index.js +11 -0
  71. package/bundled/gateway/node_modules/express/node_modules/qs/lib/parse.js +263 -0
  72. package/bundled/gateway/node_modules/express/node_modules/qs/lib/stringify.js +326 -0
  73. package/bundled/gateway/node_modules/express/node_modules/qs/lib/utils.js +252 -0
  74. package/bundled/gateway/node_modules/express/node_modules/qs/package.json +77 -0
  75. package/bundled/gateway/node_modules/express/node_modules/qs/test/parse.js +855 -0
  76. package/bundled/gateway/node_modules/express/node_modules/qs/test/stringify.js +909 -0
  77. package/bundled/gateway/node_modules/express/node_modules/qs/test/utils.js +136 -0
  78. package/bundled/gateway/node_modules/express/package.json +17 -21
  79. package/bundled/gateway/node_modules/finalhandler/HISTORY.md +0 -21
  80. package/bundled/gateway/node_modules/finalhandler/README.md +2 -2
  81. package/bundled/gateway/node_modules/finalhandler/index.js +2 -7
  82. package/bundled/gateway/node_modules/finalhandler/node_modules/encodeurl/HISTORY.md +14 -0
  83. package/bundled/gateway/node_modules/finalhandler/node_modules/encodeurl/LICENSE +22 -0
  84. package/bundled/gateway/node_modules/finalhandler/node_modules/encodeurl/README.md +128 -0
  85. package/bundled/gateway/node_modules/finalhandler/node_modules/encodeurl/index.js +60 -0
  86. package/bundled/gateway/node_modules/finalhandler/node_modules/encodeurl/package.json +40 -0
  87. package/bundled/gateway/node_modules/finalhandler/package.json +9 -10
  88. package/bundled/gateway/node_modules/form-data/CHANGELOG.md +29 -2
  89. package/bundled/gateway/node_modules/form-data/README.md +4 -4
  90. package/bundled/gateway/node_modules/form-data/lib/form_data.js +14 -2
  91. package/bundled/gateway/node_modules/form-data/package.json +7 -7
  92. package/bundled/gateway/node_modules/http-errors/HISTORY.md +0 -6
  93. package/bundled/gateway/node_modules/http-errors/index.js +3 -4
  94. package/bundled/gateway/node_modules/http-errors/package.json +8 -12
  95. package/bundled/gateway/node_modules/raw-body/HISTORY.md +308 -0
  96. package/bundled/gateway/node_modules/raw-body/SECURITY.md +24 -0
  97. package/bundled/gateway/node_modules/raw-body/package.json +7 -5
  98. package/bundled/gateway/node_modules/semver/classes/range.js +17 -4
  99. package/bundled/gateway/node_modules/semver/package.json +2 -2
  100. package/bundled/gateway/node_modules/send/HISTORY.md +7 -19
  101. package/bundled/gateway/node_modules/send/node_modules/encodeurl/HISTORY.md +14 -0
  102. package/bundled/gateway/node_modules/send/node_modules/encodeurl/LICENSE +22 -0
  103. package/bundled/gateway/node_modules/send/node_modules/encodeurl/README.md +128 -0
  104. package/bundled/gateway/node_modules/send/node_modules/encodeurl/index.js +60 -0
  105. package/bundled/gateway/node_modules/send/node_modules/encodeurl/package.json +40 -0
  106. package/bundled/gateway/node_modules/send/package.json +6 -6
  107. package/bundled/gateway/node_modules/serve-static/HISTORY.md +0 -16
  108. package/bundled/gateway/node_modules/serve-static/node_modules/encodeurl/HISTORY.md +14 -0
  109. package/bundled/gateway/node_modules/serve-static/node_modules/encodeurl/LICENSE +22 -0
  110. package/bundled/gateway/node_modules/serve-static/node_modules/encodeurl/README.md +128 -0
  111. package/bundled/gateway/node_modules/serve-static/node_modules/encodeurl/index.js +60 -0
  112. package/bundled/gateway/node_modules/serve-static/node_modules/encodeurl/package.json +40 -0
  113. package/bundled/gateway/node_modules/serve-static/package.json +3 -3
  114. package/bundled/gateway/node_modules/side-channel/CHANGELOG.md +10 -0
  115. package/bundled/gateway/node_modules/side-channel/README.md +1 -1
  116. package/bundled/gateway/node_modules/side-channel/index.js +5 -2
  117. package/bundled/gateway/node_modules/side-channel/package.json +10 -10
  118. package/bundled/gateway/node_modules/side-channel/test/index.js +16 -0
  119. package/bundled/gateway/node_modules/statuses/HISTORY.md +0 -5
  120. package/bundled/gateway/node_modules/statuses/README.md +0 -3
  121. package/bundled/gateway/node_modules/statuses/package.json +7 -7
  122. package/bundled/gateway/package-lock.json +150 -101
  123. package/bundled/gateway/package.json +31 -5
  124. package/bundled/gateway/services/gateway/src/controllers/anthropicController.js +1 -1
  125. package/bundled/gateway/services/gateway/src/controllers/awsBedrockController.js +5 -1
  126. package/bundled/gateway/services/gateway/src/index.js +14 -0
  127. package/bundled/gateway/services/gateway/src/plugins/pseudonymization/detectors/index.js +23 -1
  128. package/bundled/gateway/services/gateway/src/plugins/pseudonymization/detectors/regexDetectors.js +149 -10
  129. package/bundled/gateway/services/gateway/src/plugins/pseudonymization/entityToggles.d.ts +24 -0
  130. package/bundled/gateway/services/gateway/src/plugins/pseudonymization/entityToggles.js +54 -0
  131. package/bundled/gateway/services/gateway/src/plugins/pseudonymization/index.d.ts +2 -1
  132. package/bundled/gateway/services/gateway/src/plugins/pseudonymization/index.js +441 -92
  133. package/bundled/gateway/services/gateway/src/plugins/pseudonymization/replacementMap.d.ts +24 -0
  134. package/bundled/gateway/services/gateway/src/plugins/pseudonymization/replacementMap.js +93 -4
  135. package/bundled/gateway/services/gateway/src/plugins/pseudonymization/replacer.d.ts +22 -0
  136. package/bundled/gateway/services/gateway/src/plugins/pseudonymization/replacer.js +77 -0
  137. package/bundled/gateway/services/gateway/src/plugins/pseudonymization/streamBuffer.js +26 -8
  138. package/bundled/gateway/services/gateway/src/services/awsBedrockService.js +118 -46
  139. package/bundled/gateway/services/gateway/src/services/betaFlagQuarantine.d.ts +18 -0
  140. package/bundled/gateway/services/gateway/src/services/betaFlagQuarantine.js +111 -0
  141. package/bundled/gateway/services/gateway/src/services/configService.d.ts +8 -0
  142. package/bundled/gateway/services/gateway/src/services/configService.js +61 -53
  143. package/bundled/gateway/services/gateway/src/utils/bedrockStreamParser.d.ts +9 -0
  144. package/bundled/gateway/services/gateway/src/utils/bedrockStreamParser.js +69 -3
  145. package/bundled/gateway/services/gateway/src/utils/betaFeatureFilter.d.ts +31 -0
  146. package/bundled/gateway/services/gateway/src/utils/betaFeatureFilter.js +58 -0
  147. package/bundled/gateway/services/gateway/src/utils/payloadLogger.d.ts +6 -0
  148. package/bundled/gateway/services/gateway/src/utils/payloadLogger.js +11 -7
  149. package/bundled/gateway/services/gateway/src/utils/pluginCacheSelector.d.ts +15 -0
  150. package/bundled/gateway/services/gateway/src/utils/pluginCacheSelector.js +24 -0
  151. package/bundled/gateway/services/gateway/src/utils/sseWriter.js +4 -2
  152. package/bundled/gateway/services/gateway/src/utils/upstreamErrorBody.d.ts +10 -0
  153. package/bundled/gateway/services/gateway/src/utils/upstreamErrorBody.js +71 -0
  154. package/bundled/ollama/node_modules/.package-lock.json +111 -92
  155. package/bundled/ollama/node_modules/axios/CHANGELOG.md +0 -42
  156. package/bundled/ollama/node_modules/axios/README.md +237 -237
  157. package/bundled/ollama/node_modules/axios/dist/axios.js +21 -113
  158. package/bundled/ollama/node_modules/axios/dist/axios.js.map +1 -0
  159. package/bundled/ollama/node_modules/axios/dist/axios.min.js +2 -2
  160. package/bundled/ollama/node_modules/axios/dist/axios.min.js.map +1 -1
  161. package/bundled/ollama/node_modules/axios/dist/browser/axios.cjs +15 -121
  162. package/bundled/ollama/node_modules/axios/dist/browser/axios.cjs.map +1 -0
  163. package/bundled/ollama/node_modules/axios/dist/esm/axios.js +15 -121
  164. package/bundled/ollama/node_modules/axios/dist/esm/axios.js.map +1 -0
  165. package/bundled/ollama/node_modules/axios/dist/esm/axios.min.js +2 -2
  166. package/bundled/ollama/node_modules/axios/dist/esm/axios.min.js.map +1 -1
  167. package/bundled/ollama/node_modules/axios/dist/node/axios.cjs +107 -249
  168. package/bundled/ollama/node_modules/axios/dist/node/axios.cjs.map +1 -0
  169. package/bundled/ollama/node_modules/axios/index.d.cts +3 -8
  170. package/bundled/ollama/node_modules/axios/index.d.ts +1 -4
  171. package/bundled/ollama/node_modules/axios/lib/adapters/fetch.js +0 -79
  172. package/bundled/ollama/node_modules/axios/lib/adapters/http.js +118 -62
  173. package/bundled/ollama/node_modules/axios/lib/core/Axios.js +0 -1
  174. package/bundled/ollama/node_modules/axios/lib/core/AxiosHeaders.js +2 -2
  175. package/bundled/ollama/node_modules/axios/lib/defaults/transitional.js +0 -1
  176. package/bundled/ollama/node_modules/axios/lib/env/data.js +1 -1
  177. package/bundled/ollama/node_modules/axios/lib/helpers/buildURL.js +1 -1
  178. package/bundled/ollama/node_modules/axios/lib/helpers/formDataToStream.js +2 -2
  179. package/bundled/ollama/node_modules/axios/lib/helpers/resolveConfig.js +6 -12
  180. package/bundled/ollama/node_modules/axios/lib/helpers/toFormData.js +1 -1
  181. package/bundled/ollama/node_modules/axios/lib/utils.js +2 -23
  182. package/bundled/ollama/node_modules/axios/package.json +13 -29
  183. package/bundled/ollama/node_modules/body-parser/HISTORY.md +0 -14
  184. package/bundled/ollama/node_modules/body-parser/SECURITY.md +25 -0
  185. package/bundled/ollama/node_modules/body-parser/lib/types/json.js +5 -1
  186. package/bundled/ollama/node_modules/body-parser/lib/types/urlencoded.js +15 -7
  187. package/bundled/ollama/node_modules/body-parser/package.json +10 -9
  188. package/bundled/ollama/node_modules/cookie/index.js +35 -24
  189. package/bundled/ollama/node_modules/cookie/package.json +1 -1
  190. package/bundled/ollama/node_modules/cookie-signature/History.md +1 -5
  191. package/bundled/ollama/node_modules/cookie-signature/index.js +6 -6
  192. package/bundled/ollama/node_modules/cookie-signature/package.json +2 -2
  193. package/bundled/ollama/node_modules/express/History.md +0 -43
  194. package/bundled/ollama/node_modules/express/lib/response.js +0 -1
  195. package/bundled/ollama/node_modules/express/lib/utils.js +1 -2
  196. package/bundled/ollama/node_modules/express/package.json +17 -21
  197. package/bundled/ollama/node_modules/finalhandler/HISTORY.md +0 -21
  198. package/bundled/ollama/node_modules/finalhandler/README.md +2 -2
  199. package/bundled/ollama/node_modules/finalhandler/index.js +2 -7
  200. package/bundled/ollama/node_modules/finalhandler/node_modules/encodeurl/HISTORY.md +14 -0
  201. package/bundled/ollama/node_modules/finalhandler/node_modules/encodeurl/LICENSE +22 -0
  202. package/bundled/ollama/node_modules/finalhandler/node_modules/encodeurl/README.md +128 -0
  203. package/bundled/ollama/node_modules/finalhandler/node_modules/encodeurl/index.js +60 -0
  204. package/bundled/ollama/node_modules/finalhandler/node_modules/encodeurl/package.json +40 -0
  205. package/bundled/ollama/node_modules/finalhandler/package.json +9 -10
  206. package/bundled/ollama/node_modules/form-data/CHANGELOG.md +29 -2
  207. package/bundled/ollama/node_modules/form-data/README.md +4 -4
  208. package/bundled/ollama/node_modules/form-data/lib/form_data.js +14 -2
  209. package/bundled/ollama/node_modules/form-data/package.json +7 -7
  210. package/bundled/ollama/node_modules/http-errors/HISTORY.md +0 -6
  211. package/bundled/ollama/node_modules/http-errors/index.js +3 -4
  212. package/bundled/ollama/node_modules/http-errors/package.json +8 -12
  213. package/bundled/ollama/node_modules/raw-body/HISTORY.md +308 -0
  214. package/bundled/ollama/node_modules/raw-body/SECURITY.md +24 -0
  215. package/bundled/ollama/node_modules/raw-body/package.json +7 -5
  216. package/bundled/ollama/node_modules/send/HISTORY.md +7 -19
  217. package/bundled/ollama/node_modules/send/node_modules/encodeurl/HISTORY.md +14 -0
  218. package/bundled/ollama/node_modules/send/node_modules/encodeurl/LICENSE +22 -0
  219. package/bundled/ollama/node_modules/send/node_modules/encodeurl/README.md +128 -0
  220. package/bundled/ollama/node_modules/send/node_modules/encodeurl/index.js +60 -0
  221. package/bundled/ollama/node_modules/send/node_modules/encodeurl/package.json +40 -0
  222. package/bundled/ollama/node_modules/send/package.json +6 -6
  223. package/bundled/ollama/node_modules/serve-static/HISTORY.md +0 -16
  224. package/bundled/ollama/node_modules/serve-static/node_modules/encodeurl/HISTORY.md +14 -0
  225. package/bundled/ollama/node_modules/serve-static/node_modules/encodeurl/LICENSE +22 -0
  226. package/bundled/ollama/node_modules/serve-static/node_modules/encodeurl/README.md +128 -0
  227. package/bundled/ollama/node_modules/serve-static/node_modules/encodeurl/index.js +60 -0
  228. package/bundled/ollama/node_modules/serve-static/node_modules/encodeurl/package.json +40 -0
  229. package/bundled/ollama/node_modules/serve-static/package.json +3 -3
  230. package/bundled/ollama/node_modules/side-channel/CHANGELOG.md +10 -0
  231. package/bundled/ollama/node_modules/side-channel/README.md +1 -1
  232. package/bundled/ollama/node_modules/side-channel/index.js +5 -2
  233. package/bundled/ollama/node_modules/side-channel/package.json +10 -10
  234. package/bundled/ollama/node_modules/side-channel/test/index.js +16 -0
  235. package/bundled/ollama/node_modules/statuses/HISTORY.md +0 -5
  236. package/bundled/ollama/node_modules/statuses/README.md +0 -3
  237. package/bundled/ollama/node_modules/statuses/package.json +7 -7
  238. package/bundled/ollama/package-lock.json +114 -95
  239. package/bundled/ollama/package.json +32 -4
  240. package/dist/templates/api_config.template.json +266 -18
  241. package/node_modules/@sap-llm-gateway/service-key-parser/package.json +1 -1
  242. package/package.json +2 -2
  243. package/bundled/gateway/node_modules/axios/lib/helpers/Http2Sessions.js +0 -119
  244. package/bundled/ollama/node_modules/axios/lib/helpers/Http2Sessions.js +0 -119
@@ -0,0 +1,111 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getQuarantinedFlags = getQuarantinedFlags;
4
+ exports.quarantineFlags = quarantineFlags;
5
+ exports.clearQuarantine = clearQuarantine;
6
+ exports.isInvalidBetaFlagError = isInvalidBetaFlagError;
7
+ exports.resolveRejectedFlags = resolveRejectedFlags;
8
+ exports.recordBetaFlagRejection = recordBetaFlagRejection;
9
+ /**
10
+ * In-memory quarantine for anthropic_beta flags rejected by SAP AI Core.
11
+ *
12
+ * When SAP (AWS Bedrock behind it) rejects a request with HTTP 400
13
+ * "invalid beta flag", the flags that were sent are quarantined for that
14
+ * model so subsequent requests omit them and succeed. Keyed by modelId
15
+ * because Bedrock's flag acceptance differs per model version.
16
+ *
17
+ * Process-local and reset on restart — a self-healing stopgap, not
18
+ * configuration. Entries also auto-expire after 30 minutes so a
19
+ * false-positive detection can't silently degrade capabilities forever.
20
+ * Operators should promote quarantined flags to
21
+ * anthropic.excluded_beta_headers (admin UI, hot-reloaded) permanently.
22
+ */
23
+ const logger_1 = require("@libs/logger");
24
+ const logger = (0, logger_1.getDefaultLogger)();
25
+ // False positives happen: an unrelated 400 whose body merely mentions
26
+ // "anthropic_beta" would otherwise quarantine valid flags forever (until
27
+ // pod restart), silently degrading capabilities like the 1M context window.
28
+ // A 30-minute TTL bounds the blast radius of any single misdetection. The
29
+ // permanent mechanism for genuinely unsupported flags is the config
30
+ // denylist (anthropic.excluded_beta_headers), not this in-memory cache.
31
+ const QUARANTINE_TTL_MS = 30 * 60 * 1000;
32
+ const quarantinedByModel = new Map();
33
+ function getQuarantinedFlags(modelId) {
34
+ const flags = quarantinedByModel.get(modelId);
35
+ if (!flags) {
36
+ return [];
37
+ }
38
+ const now = Date.now();
39
+ const live = [];
40
+ for (const [flag, quarantinedAt] of flags) {
41
+ if (now - quarantinedAt >= QUARANTINE_TTL_MS) {
42
+ flags.delete(flag);
43
+ }
44
+ else {
45
+ live.push(flag);
46
+ }
47
+ }
48
+ if (flags.size === 0) {
49
+ quarantinedByModel.delete(modelId);
50
+ }
51
+ return live;
52
+ }
53
+ function quarantineFlags(modelId, flags) {
54
+ if (flags.length === 0) {
55
+ return;
56
+ }
57
+ const now = Date.now();
58
+ const map = quarantinedByModel.get(modelId) ?? new Map();
59
+ for (const flag of flags) {
60
+ map.set(flag, now);
61
+ }
62
+ quarantinedByModel.set(modelId, map);
63
+ }
64
+ function clearQuarantine() {
65
+ quarantinedByModel.clear();
66
+ }
67
+ function serializeErrorData(errorData) {
68
+ if (typeof errorData === 'string') {
69
+ return errorData;
70
+ }
71
+ try {
72
+ return JSON.stringify(errorData) ?? '';
73
+ }
74
+ catch {
75
+ return '';
76
+ }
77
+ }
78
+ /**
79
+ * Whether an upstream error response is a beta-flag rejection.
80
+ */
81
+ function isInvalidBetaFlagError(status, errorData) {
82
+ if (status !== 400) {
83
+ return false;
84
+ }
85
+ return /invalid beta flag|anthropic[-_]beta/i.test(serializeErrorData(errorData));
86
+ }
87
+ /**
88
+ * Flags to quarantine: those explicitly named in the error body when present
89
+ * (first-party-style errors name them), else all flags that were sent
90
+ * (Bedrock's bare "invalid beta flag" names none).
91
+ */
92
+ function resolveRejectedFlags(errorData, sentFlags) {
93
+ if (sentFlags.length === 0) {
94
+ return [];
95
+ }
96
+ const serialized = serializeErrorData(errorData);
97
+ const named = sentFlags.filter(flag => serialized.includes(flag));
98
+ return named.length > 0 ? named : [...sentFlags];
99
+ }
100
+ /**
101
+ * Record a beta-flag rejection so subsequent requests for this model omit
102
+ * the offending flags. Returns the quarantined flags.
103
+ */
104
+ function recordBetaFlagRejection(modelId, errorData, sentFlags) {
105
+ const rejected = resolveRejectedFlags(errorData, sentFlags);
106
+ quarantineFlags(modelId, rejected);
107
+ logger.warn('BetaFlagQuarantine', `Quarantined ${rejected.length} beta flag(s) for model ${modelId} after upstream 400: ${rejected.join(', ')}. ` +
108
+ `Subsequent requests omit them (in-memory, resets on restart or after 30 minutes). ` +
109
+ `Add them to anthropic.excluded_beta_headers via the admin UI to make this permanent.`);
110
+ return rejected;
111
+ }
@@ -111,6 +111,13 @@ export declare const getAnthropicBedrockVersion: () => string;
111
111
  * @returns Array of beta header values to filter out
112
112
  */
113
113
  export declare const getExcludedBetaHeaders: () => string[];
114
+ /**
115
+ * Get supported (allowlisted) beta headers for Anthropic requests to SAP AI Core.
116
+ * When non-empty, only these beta flags are forwarded; the excluded_beta_headers
117
+ * denylist is still applied on top. Empty/absent means no allowlist filtering.
118
+ * @returns Array of allowlisted beta header values
119
+ */
120
+ export declare const getSupportedBetaHeaders: () => string[];
114
121
  export declare const getOpenAIDeploymentApiVersion: () => string | undefined;
115
122
  /**
116
123
  * Get all provider configurations
@@ -201,6 +208,7 @@ declare const _default: {
201
208
  shouldEmulateStreaming: (provider: string, modelName: string) => boolean;
202
209
  getAnthropicBedrockVersion: () => string;
203
210
  getExcludedBetaHeaders: () => string[];
211
+ getSupportedBetaHeaders: () => string[];
204
212
  getOpenAIDeploymentApiVersion: () => string | undefined;
205
213
  getAllProviderConfigs: () => ApiConfig;
206
214
  getModelListChanges: () => ModelListChanges;
@@ -36,7 +36,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
36
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
- exports.getAccessToken = exports.getDeploymentIdCacheStatus = exports.clearDeploymentIdCache = exports.getDeploymentId = exports.getSAPAICoreConfig = exports.getHookConfig = exports.get = exports.getTimeout = exports.getCachePricingForModel = exports.getModelListChanges = exports.getAllProviderConfigs = exports.getOpenAIDeploymentApiVersion = exports.getExcludedBetaHeaders = exports.getAnthropicBedrockVersion = exports.shouldEmulateStreaming = exports.getOriginalModel = exports.getSubstitutedModel = exports.patchConfig = exports.updateConfig = exports.getConfigAsync = exports.getConfig = exports.isConfigurationReady = exports.onConfigurationReady = exports.publishModelList = void 0;
39
+ exports.getAccessToken = exports.getDeploymentIdCacheStatus = exports.clearDeploymentIdCache = exports.getDeploymentId = exports.getSAPAICoreConfig = exports.getHookConfig = exports.get = exports.getTimeout = exports.getCachePricingForModel = exports.getModelListChanges = exports.getAllProviderConfigs = exports.getOpenAIDeploymentApiVersion = exports.getSupportedBetaHeaders = exports.getExcludedBetaHeaders = exports.getAnthropicBedrockVersion = exports.shouldEmulateStreaming = exports.getOriginalModel = exports.getSubstitutedModel = exports.patchConfig = exports.updateConfig = exports.getConfigAsync = exports.getConfig = exports.isConfigurationReady = exports.onConfigurationReady = exports.publishModelList = void 0;
40
40
  /**
41
41
  * Service to manage API provider configurations including model substitutions and streaming emulation
42
42
  */
@@ -48,6 +48,7 @@ const pluginLoader_1 = __importDefault(require("./pluginLoader"));
48
48
  const logger_1 = require("@libs/logger");
49
49
  const logger = (0, logger_1.getDefaultLogger)();
50
50
  const unifiedAuthConfig_1 = require("../config/unifiedAuthConfig");
51
+ const pluginCacheSelector_1 = require("../utils/pluginCacheSelector");
51
52
  // Default configuration path
52
53
  const CONFIG_FILE_PATH = process.env.CONFIG_FILE_PATH || path.join(process.cwd(), 'api_config.json');
53
54
  // Backup configuration
@@ -66,7 +67,11 @@ let valkeySubscriber = null;
66
67
  let valkeyPublisher = null;
67
68
  let valkeyInitialized = false;
68
69
  // Event channels
69
- const CONFIG_CHANGE_CHANNEL = 'sap-llm-gateway:config-changed';
70
+ // Env-overridable so integration tests can isolate a spawned gateway on a
71
+ // test-scoped channel instead of broadcasting on the production channel (a
72
+ // test publishing here once wiped the live gateway's config mid-session —
73
+ // Valkey pub/sub channels are global, they are NOT scoped by database index).
74
+ const CONFIG_CHANGE_CHANNEL = process.env.CONFIG_CHANGE_CHANNEL || 'sap-llm-gateway:config-changed';
70
75
  const MODEL_LIST_CHANNEL = 'sap-llm-gateway:model-list-updated';
71
76
  const STARTUP_READY_CHANNEL = 'sap-llm-gateway:service-ready';
72
77
  // Startup coordination
@@ -389,6 +394,37 @@ const requestInitialConfiguration = async () => {
389
394
  logger.warn('ConfigService', `Failed to request initial configuration: ${error.message}`);
390
395
  }
391
396
  };
397
+ /**
398
+ * Reload plugin modules on a configuration change.
399
+ *
400
+ * Evicts ONLY the plugins subtree from require.cache (plugin entry files plus
401
+ * their plugin-internal helper modules, e.g. plugins/pseudonymization/*), then
402
+ * lets pluginLoader rebuild the registry. Gateway SERVICE modules are never
403
+ * evicted: doing so caused split-brain state — Express handlers registered at
404
+ * startup kept referencing the old module instances while later require()
405
+ * calls created fresh ones with empty caches, and the two worlds disagreed
406
+ * (observed as model substitution silently failing after a config-activation
407
+ * event until a manual restart). Service code changes are picked up by nodemon
408
+ * (dev) or a deploy restart (prod), never by config pushes.
409
+ */
410
+ const reloadPluginModules = () => {
411
+ try {
412
+ const pluginsDirPrefix = path.join(__dirname, '..', 'plugins');
413
+ const keys = (0, pluginCacheSelector_1.selectPluginCacheKeysToClear)(Object.keys(require.cache), pluginsDirPrefix);
414
+ for (const key of keys) {
415
+ delete require.cache[key];
416
+ }
417
+ if (keys.length > 0) {
418
+ logger.info('ConfigService', `Cleared require cache for ${keys.length} plugin modules`);
419
+ }
420
+ const pluginLoader = require('./pluginLoader');
421
+ pluginLoader.reloadAll('./src/plugins');
422
+ logger.info('ConfigService', 'Plugins reloaded successfully');
423
+ }
424
+ catch (pluginError) {
425
+ logger.error('ConfigService', `Error reloading plugins: ${pluginError.message}`);
426
+ }
427
+ };
392
428
  /**
393
429
  * Trigger configuration reload mechanisms (plugins, model service cache, etc.)
394
430
  */
@@ -399,32 +435,8 @@ const triggerConfigurationReload = async () => {
399
435
  logger.reinitialize();
400
436
  logger.info('ConfigService', 'Logger reinitialized with updated configuration');
401
437
  }
402
- // Reload plugins and all gateway service modules to pick up code changes on disk
403
- try {
404
- // Clear require cache for all gateway modules so updated code on disk is loaded.
405
- // This enables hot-reload of service files during config push without a full process restart.
406
- // Exclude configService itself — it owns startup coordination state (configurationReceived,
407
- // isWaitingForAdminEvents, startupRequestSent, cachedConfig) that must survive reloads.
408
- const gatewayDistPrefix = path.join(__dirname, '..').replace(/\\/g, '/');
409
- const ownModulePath = __filename.replace(/\\/g, '/');
410
- let cleared = 0;
411
- for (const key of Object.keys(require.cache)) {
412
- const normalizedKey = key.replace(/\\/g, '/');
413
- if (normalizedKey.startsWith(gatewayDistPrefix) && !normalizedKey.includes('node_modules') && normalizedKey !== ownModulePath) {
414
- delete require.cache[key];
415
- cleared++;
416
- }
417
- }
418
- if (cleared > 0) {
419
- logger.info('ConfigService', `Cleared require cache for ${cleared} gateway modules`);
420
- }
421
- const pluginLoader = require('./pluginLoader');
422
- pluginLoader.reloadAll('./src/plugins');
423
- logger.info('ConfigService', 'Plugins reloaded successfully');
424
- }
425
- catch (pluginError) {
426
- logger.error('ConfigService', `Error reloading plugins: ${pluginError.message}`);
427
- }
438
+ // Reload PLUGIN modules to pick up plugin code/config changes.
439
+ reloadPluginModules();
428
440
  // Clear model service caches to force reapplication of config
429
441
  try {
430
442
  const modelService = require('./modelService').default;
@@ -789,31 +801,9 @@ const updateConfig = (newConfig) => {
789
801
  logger.reinitialize();
790
802
  logger.info('ConfigService', 'Logger reinitialized with updated configuration');
791
803
  }
792
- // Reload plugins and all gateway service modules to pick up code changes on disk
793
- try {
794
- // Clear require cache for all gateway modules so updated code on disk is loaded.
795
- // This enables hot-reload of service files during config push without a full process restart.
796
- // Exclude configService itself — it owns startup coordination state that must survive reloads.
797
- const gatewayDistPrefix = path.join(__dirname, '..').replace(/\\/g, '/');
798
- const ownModulePath = __filename.replace(/\\/g, '/');
799
- let cleared = 0;
800
- for (const key of Object.keys(require.cache)) {
801
- const normalizedKey = key.replace(/\\/g, '/');
802
- if (normalizedKey.startsWith(gatewayDistPrefix) && !normalizedKey.includes('node_modules') && normalizedKey !== ownModulePath) {
803
- delete require.cache[key];
804
- cleared++;
805
- }
806
- }
807
- if (cleared > 0) {
808
- logger.info('ConfigService', `Cleared require cache for ${cleared} gateway modules`);
809
- }
810
- const pluginLoader = require('./pluginLoader');
811
- pluginLoader.reloadAll('./src/plugins');
812
- logger.info('ConfigService', 'Plugins reloaded successfully');
813
- }
814
- catch (pluginError) {
815
- logger.error('ConfigService', `Error reloading plugins: ${pluginError.message}`);
816
- }
804
+ // Reload PLUGIN modules to pick up plugin code/config changes (services are
805
+ // never evicted — see reloadPluginModules for the split-brain rationale).
806
+ reloadPluginModules();
817
807
  // Clear model service caches to force reapplication of config
818
808
  try {
819
809
  const modelService = require('./modelService').default;
@@ -973,6 +963,23 @@ const getExcludedBetaHeaders = () => {
973
963
  }
974
964
  };
975
965
  exports.getExcludedBetaHeaders = getExcludedBetaHeaders;
966
+ /**
967
+ * Get supported (allowlisted) beta headers for Anthropic requests to SAP AI Core.
968
+ * When non-empty, only these beta flags are forwarded; the excluded_beta_headers
969
+ * denylist is still applied on top. Empty/absent means no allowlist filtering.
970
+ * @returns Array of allowlisted beta header values
971
+ */
972
+ const getSupportedBetaHeaders = () => {
973
+ try {
974
+ const config = (0, exports.getConfig)();
975
+ return config?.api_config?.anthropic?.supported_beta_headers || [];
976
+ }
977
+ catch (error) {
978
+ logger.error('ConfigService', `Error getting supported beta headers: ${error.message}`);
979
+ return [];
980
+ }
981
+ };
982
+ exports.getSupportedBetaHeaders = getSupportedBetaHeaders;
976
983
  const getOpenAIDeploymentApiVersion = () => {
977
984
  try {
978
985
  const config = (0, exports.getConfig)();
@@ -1284,6 +1291,7 @@ exports.default = {
1284
1291
  shouldEmulateStreaming: exports.shouldEmulateStreaming,
1285
1292
  getAnthropicBedrockVersion: exports.getAnthropicBedrockVersion,
1286
1293
  getExcludedBetaHeaders: exports.getExcludedBetaHeaders,
1294
+ getSupportedBetaHeaders: exports.getSupportedBetaHeaders,
1287
1295
  getOpenAIDeploymentApiVersion: exports.getOpenAIDeploymentApiVersion,
1288
1296
  getAllProviderConfigs: exports.getAllProviderConfigs,
1289
1297
  getModelListChanges: exports.getModelListChanges,
@@ -32,6 +32,15 @@ declare class BedrockStreamParser {
32
32
  * Helper to create a content_block_start event
33
33
  */
34
34
  private createContentBlockStartEvent;
35
+ /**
36
+ * Emit an event that is already in Anthropic Messages streaming format with
37
+ * proper `event: <type>` SSE framing. Records upstream message id and terminal
38
+ * usage metrics into streamState so downstream bookkeeping (usage tracking,
39
+ * end-of-stream finalization) stays consistent. Passthrough events never feed
40
+ * the tool-fragment reconstruction buffers — those exist only for Bedrock
41
+ * camelCase envelope streams.
42
+ */
43
+ private emitAnthropicPassthroughEvent;
35
44
  /**
36
45
  * Handle end of stream
37
46
  */
@@ -37,6 +37,19 @@ const eventsource_parser_1 = require("eventsource-parser");
37
37
  const sseWriter = __importStar(require("./sseWriter"));
38
38
  const logger_1 = require("@libs/logger");
39
39
  const logger = (0, logger_1.getDefaultLogger)();
40
+ // Anthropic Messages streaming event types. SAP native invoke-with-response-stream
41
+ // for deployed Anthropic models emits events ALREADY in this format — they must be
42
+ // passed through with proper `event: <type>` SSE framing, not transformed.
43
+ const ANTHROPIC_STREAM_EVENTS = new Set([
44
+ 'message_start',
45
+ 'content_block_start',
46
+ 'content_block_delta',
47
+ 'content_block_stop',
48
+ 'message_delta',
49
+ 'message_stop',
50
+ 'ping',
51
+ 'error',
52
+ ]);
40
53
  /**
41
54
  * Handles streaming responses from AWS Bedrock, properly parsing SSE events and processing JSON fragments
42
55
  */
@@ -61,7 +74,8 @@ class BedrockStreamParser {
61
74
  finalFirstByteLatency: null,
62
75
  // Tool fragment buffering state
63
76
  toolFragmentBuffers: new Map(), // Map of contentBlockIndex -> array of fragments
64
- processedFragmentCount: 0
77
+ processedFragmentCount: 0,
78
+ terminalMessageStopEmitted: false
65
79
  };
66
80
  // Create our event source parser with the correct v3.0.2 API
67
81
  this.parser = (0, eventsource_parser_1.createParser)({
@@ -103,6 +117,10 @@ class BedrockStreamParser {
103
117
  return;
104
118
  }
105
119
  // Check if this is an SSE data chunk
120
+ // TODO: this heuristic misroutes network chunks that split an SSE event mid-JSON
121
+ // (a fragment not starting with 'data:'/'event:' is treated as direct data and can
122
+ // emit corrupted events). parser.feed() below already reassembles across chunk
123
+ // boundaries — partial SSE fragments should be fed to it rather than handleData.
106
124
  const isSseFormat = chunkStr.startsWith('data:') ||
107
125
  chunkStr.includes('\ndata:') ||
108
126
  chunkStr.includes('event:') ||
@@ -277,6 +295,17 @@ class BedrockStreamParser {
277
295
  return;
278
296
  }
279
297
  }
298
+ // Already-Anthropic events (SAP native streams for deployed Anthropic models)
299
+ // must be passed through with proper `event: <type>` SSE framing.
300
+ // transformBedrockEvent only understands the Bedrock camelCase envelope, so
301
+ // until this branch existed these all fell through to the raw `data:`-only
302
+ // fallback below — SSE without `event:` headers, which claude-cli >= 2.1.215
303
+ // cannot parse (perceived as a lost connection / ECONNRESET on every stream).
304
+ if (typeof bedrockEvent.type === 'string'
305
+ && ANTHROPIC_STREAM_EVENTS.has(bedrockEvent.type)) {
306
+ this.emitAnthropicPassthroughEvent(bedrockEvent);
307
+ return;
308
+ }
280
309
  // Transform the event
281
310
  const events = this.transformBedrockEvent(bedrockEvent);
282
311
  if (events && events.length > 0) {
@@ -338,14 +367,51 @@ class BedrockStreamParser {
338
367
  this.streamState.emittedToolUseContentStartIndices.add(index);
339
368
  }
340
369
  }
370
+ /**
371
+ * Emit an event that is already in Anthropic Messages streaming format with
372
+ * proper `event: <type>` SSE framing. Records upstream message id and terminal
373
+ * usage metrics into streamState so downstream bookkeeping (usage tracking,
374
+ * end-of-stream finalization) stays consistent. Passthrough events never feed
375
+ * the tool-fragment reconstruction buffers — those exist only for Bedrock
376
+ * camelCase envelope streams.
377
+ */
378
+ emitAnthropicPassthroughEvent(event) {
379
+ if (!this.res || this.res.writableEnded)
380
+ return;
381
+ // Adopt the upstream message id so later fallback paths never mint a second
382
+ // message_start for a stream that already has one.
383
+ if (event.type === 'message_start' && event.message?.id && !this.streamState.messageId) {
384
+ this.streamState.messageId = event.message.id;
385
+ }
386
+ if (event.type === 'message_stop') {
387
+ const metrics = event['amazon-bedrock-invocationMetrics'];
388
+ if (metrics) {
389
+ this.streamState.finalInputTokens = metrics.inputTokenCount || 0;
390
+ this.streamState.finalOutputTokens = metrics.outputTokenCount || 0;
391
+ this.streamState.finalCacheCreationTokens = metrics.cacheWriteInputTokenCount || 0;
392
+ this.streamState.finalCacheReadTokens = metrics.cacheReadInputTokenCount || 0;
393
+ this.streamState.finalLatencyMs = metrics.invocationLatency || 0;
394
+ this.streamState.finalFirstByteLatency = metrics.firstByteLatency || 0;
395
+ if (this.outputFormat === 'anthropic') {
396
+ // Anthropic clients get a clean message_stop; the metrics live in streamState.
397
+ const { ['amazon-bedrock-invocationMetrics']: _metrics, ...clean } = event;
398
+ event = clean;
399
+ }
400
+ }
401
+ this.streamState.terminalMessageStopEmitted = true;
402
+ }
403
+ logger.debug('BedrockStreamParser', `Passthrough Anthropic event: ${event.type}`);
404
+ sseWriter.writeEventStream(this.res, event.type, JSON.stringify(event));
405
+ }
341
406
  /**
342
407
  * Handle end of stream
343
408
  */
344
409
  handleDone() {
345
410
  // Process any remaining tool fragments in buffers
346
411
  this.processAllToolFragmentBuffers();
347
- // Send final message_stop if needed
348
- if (this.streamState.messageId && this.res && !this.res.writableEnded) {
412
+ // Send final message_stop if needed (never after a passthrough already sent one)
413
+ if (this.streamState.messageId && !this.streamState.terminalMessageStopEmitted
414
+ && this.res && !this.res.writableEnded) {
349
415
  if (this.outputFormat === 'anthropic') {
350
416
  // Anthropic format: clean message_stop without Bedrock-specific metrics
351
417
  sseWriter.writeEventStream(this.res, 'message_stop', JSON.stringify({
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Pure helpers for assembling and filtering anthropic_beta feature flags
3
+ * before forwarding requests to SAP AI Core Bedrock deployments.
4
+ *
5
+ * Filtering semantics:
6
+ * 1. Allowlist (supported): when non-empty, only listed flags survive.
7
+ * An empty/absent allowlist means "no allowlist filtering" (legacy behavior).
8
+ * 2. Denylist (excluded): always applied on top of the allowlist result.
9
+ *
10
+ * @see api_config.json - anthropic.supported_beta_headers / anthropic.excluded_beta_headers
11
+ */
12
+ export interface BetaFilterOptions {
13
+ /** Allowlist from api_config.anthropic.supported_beta_headers; [] disables allowlist filtering */
14
+ supported: string[];
15
+ /** Denylist from api_config.anthropic.excluded_beta_headers */
16
+ excluded: string[];
17
+ }
18
+ /**
19
+ * Parse the raw anthropic-beta header value into a clean array of flags.
20
+ * Accepts a comma-separated string, an array of values (each possibly
21
+ * comma-separated), or undefined.
22
+ */
23
+ export declare function parseAnthropicBetaHeader(headerValue: string | string[] | undefined): string[];
24
+ /**
25
+ * Merge multiple flag lists, preserving first-seen order and deduplicating.
26
+ */
27
+ export declare function mergeBetaFeatures(...lists: string[][]): string[];
28
+ /**
29
+ * Apply allowlist-then-denylist filtering to a list of beta flags.
30
+ */
31
+ export declare function filterBetaFeatures(features: string[], options: BetaFilterOptions): string[];
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ /**
3
+ * Pure helpers for assembling and filtering anthropic_beta feature flags
4
+ * before forwarding requests to SAP AI Core Bedrock deployments.
5
+ *
6
+ * Filtering semantics:
7
+ * 1. Allowlist (supported): when non-empty, only listed flags survive.
8
+ * An empty/absent allowlist means "no allowlist filtering" (legacy behavior).
9
+ * 2. Denylist (excluded): always applied on top of the allowlist result.
10
+ *
11
+ * @see api_config.json - anthropic.supported_beta_headers / anthropic.excluded_beta_headers
12
+ */
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.parseAnthropicBetaHeader = parseAnthropicBetaHeader;
15
+ exports.mergeBetaFeatures = mergeBetaFeatures;
16
+ exports.filterBetaFeatures = filterBetaFeatures;
17
+ /**
18
+ * Parse the raw anthropic-beta header value into a clean array of flags.
19
+ * Accepts a comma-separated string, an array of values (each possibly
20
+ * comma-separated), or undefined.
21
+ */
22
+ function parseAnthropicBetaHeader(headerValue) {
23
+ if (!headerValue) {
24
+ return [];
25
+ }
26
+ const rawValues = Array.isArray(headerValue) ? headerValue : [headerValue];
27
+ return rawValues
28
+ .flatMap(value => String(value).split(','))
29
+ .map(flag => flag.trim())
30
+ .filter(Boolean);
31
+ }
32
+ /**
33
+ * Merge multiple flag lists, preserving first-seen order and deduplicating.
34
+ */
35
+ function mergeBetaFeatures(...lists) {
36
+ const merged = [];
37
+ for (const list of lists) {
38
+ for (const flag of list) {
39
+ if (!merged.includes(flag)) {
40
+ merged.push(flag);
41
+ }
42
+ }
43
+ }
44
+ return merged;
45
+ }
46
+ /**
47
+ * Apply allowlist-then-denylist filtering to a list of beta flags.
48
+ */
49
+ function filterBetaFeatures(features, options) {
50
+ let result = features;
51
+ if (options.supported.length > 0) {
52
+ result = result.filter(flag => options.supported.includes(flag));
53
+ }
54
+ if (options.excluded.length > 0) {
55
+ result = result.filter(flag => !options.excluded.includes(flag));
56
+ }
57
+ return result;
58
+ }
@@ -1,2 +1,8 @@
1
1
  import { Request, Response } from 'express';
2
+ /**
3
+ * Whether payload logging is currently enabled. Re-reads the dynamic config
4
+ * on every call (with the PAYLOAD_LOGGING_ENABLED env override applied), so
5
+ * an api_config.json change takes effect on the next request — no restart.
6
+ */
7
+ export declare const isPayloadLoggingEnabled: () => boolean;
2
8
  export declare const savePayload: (requestId: string | number, stage: string, data: any, req?: Request, res?: Response) => void;
@@ -36,7 +36,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
36
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
- exports.savePayload = void 0;
39
+ exports.savePayload = exports.isPayloadLoggingEnabled = void 0;
40
40
  const fs = __importStar(require("fs"));
41
41
  const path = __importStar(require("path"));
42
42
  const logger_1 = require("@libs/logger");
@@ -131,19 +131,23 @@ function ensureLogDirExists() {
131
131
  }
132
132
  return { success: true, logDir };
133
133
  }
134
+ /**
135
+ * Whether payload logging is currently enabled. Re-reads the dynamic config
136
+ * on every call (with the PAYLOAD_LOGGING_ENABLED env override applied), so
137
+ * an api_config.json change takes effect on the next request — no restart.
138
+ */
139
+ const isPayloadLoggingEnabled = () => {
140
+ return getLoggingConfig().payloadLoggingEnabled;
141
+ };
142
+ exports.isPayloadLoggingEnabled = isPayloadLoggingEnabled;
134
143
  // Enhanced payload logger with headers and HTTP method support
135
144
  const savePayload = (requestId, stage, data, req, res) => {
136
- const currentDebugEnv = process.env.DEBUG;
137
145
  const { payloadLoggingEnabled } = getLoggingConfig();
138
146
  // Skip debug logging for disabled payload logging to avoid log noise at INFO level
139
147
  if (!payloadLoggingEnabled) {
140
148
  return;
141
149
  }
142
- logger.debug('PayloadLogger', `Enhanced save called for requestId: ${requestId}, stage: ${stage}, DEBUG_ENV: '${currentDebugEnv}', PAYLOAD_LOGGING_ENABLED: ${payloadLoggingEnabled}`);
143
- if (currentDebugEnv !== 'true') {
144
- logger.debug('PayloadLogger', `Skipping enhanced save, process.env.DEBUG is not the string 'true'.`);
145
- return;
146
- }
150
+ logger.debug('PayloadLogger', `Enhanced save called for requestId: ${requestId}, stage: ${stage}, PAYLOAD_LOGGING_ENABLED: ${payloadLoggingEnabled}`);
147
151
  if (!requestId) {
148
152
  logger.debug('PayloadLogger', `Skipping enhanced save, no requestId provided.`);
149
153
  return;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Select which require.cache keys may be evicted on a configuration reload.
3
+ *
4
+ * ONLY modules under the plugins directory are eligible. Evicting gateway
5
+ * SERVICE modules on a config push causes split-brain state: Express handlers
6
+ * registered at startup keep referencing the old module instances, while any
7
+ * later require() creates fresh instances with empty caches — the two worlds
8
+ * then disagree (observed as model substitution silently failing after a
9
+ * config-activation event until a manual restart). Service code changes are
10
+ * picked up by nodemon (dev) or a deploy restart (prod), never by config
11
+ * pushes. Plugin ENTRY files are cache-cleared by pluginLoader itself; this
12
+ * selector additionally covers plugin-internal helper modules (e.g.
13
+ * plugins/pseudonymization/*.ts) that plugin entries import.
14
+ */
15
+ export declare function selectPluginCacheKeysToClear(cacheKeys: string[], pluginsDirPrefix: string): string[];
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.selectPluginCacheKeysToClear = selectPluginCacheKeysToClear;
4
+ /**
5
+ * Select which require.cache keys may be evicted on a configuration reload.
6
+ *
7
+ * ONLY modules under the plugins directory are eligible. Evicting gateway
8
+ * SERVICE modules on a config push causes split-brain state: Express handlers
9
+ * registered at startup keep referencing the old module instances, while any
10
+ * later require() creates fresh instances with empty caches — the two worlds
11
+ * then disagree (observed as model substitution silently failing after a
12
+ * config-activation event until a manual restart). Service code changes are
13
+ * picked up by nodemon (dev) or a deploy restart (prod), never by config
14
+ * pushes. Plugin ENTRY files are cache-cleared by pluginLoader itself; this
15
+ * selector additionally covers plugin-internal helper modules (e.g.
16
+ * plugins/pseudonymization/*.ts) that plugin entries import.
17
+ */
18
+ function selectPluginCacheKeysToClear(cacheKeys, pluginsDirPrefix) {
19
+ const prefix = pluginsDirPrefix.replace(/\\/g, '/').replace(/\/+$/, '') + '/';
20
+ return cacheKeys.filter(key => {
21
+ const normalized = key.replace(/\\/g, '/');
22
+ return normalized.startsWith(prefix) && !normalized.includes('node_modules');
23
+ });
24
+ }
@@ -176,8 +176,10 @@ const writePing = (res) => {
176
176
  }
177
177
  // Write a comment as ping (standard SSE practice)
178
178
  res.write(': ping\n\n');
179
- // Also write a data event for clients that might not properly handle comments
180
- res.write('data: {"type":"ping"}\n\n');
179
+ // Also write a ping event for clients that might not properly handle comments.
180
+ // Framed with `event: ping` like Anthropic's wire format — strict SSE parsers
181
+ // (claude-cli >= 2.1.215) require the event: field on every data block.
182
+ res.write('event: ping\ndata: {"type":"ping"}\n\n');
181
183
  }
182
184
  catch (writeError) {
183
185
  logger.error('SSEWriter', 'Error writing ping to client:', writeError);
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Read an upstream (axios) error body that may be a stream.
3
+ *
4
+ * Streaming requests use `responseType: 'stream'`, so on an error status axios
5
+ * delivers `error.response.data` as a Readable stream — logging it directly
6
+ * prints an unusable stream object, and body-matching logic (e.g. the beta-flag
7
+ * quarantine) sees nothing. Drain up to a small cap with a short timeout and
8
+ * parse JSON when possible; non-stream bodies are returned unchanged.
9
+ */
10
+ export declare function readUpstreamErrorBody(response: any): Promise<any>;