@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
@@ -4,13 +4,37 @@
4
4
  * Maintains forward (original → placeholder) and reverse (placeholder → original)
5
5
  * mappings for the lifetime of a single request. Ensures idempotency: the same
6
6
  * original text always gets the same placeholder within a request.
7
+ *
8
+ * Pseudonymization placeholders are CONTENT-DERIVED (truncated SHA-256 of the
9
+ * original value), so the same value produces the same token in every request:
10
+ * - tokens echoed by the model out of older conversation turns ("residue")
11
+ * resolve correctly as long as the underlying value is present in the
12
+ * current request (it re-mints the identical token);
13
+ * - the response cache can safely replay a masked response cached from an
14
+ * equivalent request — its tokens are reproducible from content alone;
15
+ * - masked request bodies are byte-stable across requests, which also keeps
16
+ * upstream prompt caching effective.
17
+ * Anonymization placeholders remain per-request counters: without a reverse
18
+ * map they are never unmasked, and a content-derived token would make the same
19
+ * entity linkable across requests, defeating anonymization.
7
20
  */
8
21
  export declare class ReplacementMap {
9
22
  readonly forward: Map<string, string>;
10
23
  readonly reverse: Map<string, string>;
11
24
  private counters;
25
+ private minCountersByPrefix;
12
26
  private method;
13
27
  constructor(method?: 'pseudonymization' | 'anonymization');
28
+ /**
29
+ * Ensure freshly minted placeholders for `prefix` start above `minCount`.
30
+ *
31
+ * Used when the incoming request already contains literal MASKED_*_n tokens
32
+ * ("residue" leaked into the conversation history by an earlier session).
33
+ * Without this, a fresh entity could mint the same number as a residue token,
34
+ * and unmasking the model's echo of that residue would substitute the WRONG
35
+ * person's name.
36
+ */
37
+ reserveMinCount(prefix: string, minCount: number): void;
14
38
  /**
15
39
  * Get or assign a placeholder for an entity
16
40
  */
@@ -5,18 +5,66 @@
5
5
  * Maintains forward (original → placeholder) and reverse (placeholder → original)
6
6
  * mappings for the lifetime of a single request. Ensures idempotency: the same
7
7
  * original text always gets the same placeholder within a request.
8
+ *
9
+ * Pseudonymization placeholders are CONTENT-DERIVED (truncated SHA-256 of the
10
+ * original value), so the same value produces the same token in every request:
11
+ * - tokens echoed by the model out of older conversation turns ("residue")
12
+ * resolve correctly as long as the underlying value is present in the
13
+ * current request (it re-mints the identical token);
14
+ * - the response cache can safely replay a masked response cached from an
15
+ * equivalent request — its tokens are reproducible from content alone;
16
+ * - masked request bodies are byte-stable across requests, which also keeps
17
+ * upstream prompt caching effective.
18
+ * Anonymization placeholders remain per-request counters: without a reverse
19
+ * map they are never unmasked, and a content-derived token would make the same
20
+ * entity linkable across requests, defeating anonymization.
8
21
  */
22
+ var __importDefault = (this && this.__importDefault) || function (mod) {
23
+ return (mod && mod.__esModule) ? mod : { "default": mod };
24
+ };
9
25
  Object.defineProperty(exports, "__esModule", { value: true });
10
26
  exports.ReplacementMap = void 0;
27
+ const crypto_1 = __importDefault(require("crypto"));
11
28
  const types_1 = require("./types");
29
+ /** Digits of the content-derived placeholder id. Decimal (not hex) on purpose:
30
+ * models copy short digit runs far more reliably than hex strings — a garbled
31
+ * id is unrecoverable, so copy fidelity is part of the security design. */
32
+ const HASH_ID_DIGITS = 8;
33
+ /**
34
+ * Derive a decimal id from a SHA-256 digest. `level` widens the id (8, 10, 12 …
35
+ * digits) using more of the digest — used for deterministic collision probing:
36
+ * the same value always yields the same ladder of candidate ids.
37
+ */
38
+ function idFromDigest(digest, level) {
39
+ const digits = HASH_ID_DIGITS + 2 * level;
40
+ const window = digest.slice(0, Math.min(12 + 4 * level, digest.length));
41
+ const num = BigInt(`0x${window}`) % (10n ** BigInt(digits));
42
+ return num.toString().padStart(digits, '0');
43
+ }
12
44
  class ReplacementMap {
13
45
  forward = new Map();
14
46
  reverse = new Map();
15
47
  counters = new Map();
48
+ minCountersByPrefix = new Map();
16
49
  method;
17
50
  constructor(method = 'pseudonymization') {
18
51
  this.method = method;
19
52
  }
53
+ /**
54
+ * Ensure freshly minted placeholders for `prefix` start above `minCount`.
55
+ *
56
+ * Used when the incoming request already contains literal MASKED_*_n tokens
57
+ * ("residue" leaked into the conversation history by an earlier session).
58
+ * Without this, a fresh entity could mint the same number as a residue token,
59
+ * and unmasking the model's echo of that residue would substitute the WRONG
60
+ * person's name.
61
+ */
62
+ reserveMinCount(prefix, minCount) {
63
+ const current = this.minCountersByPrefix.get(prefix) || 0;
64
+ if (minCount > current) {
65
+ this.minCountersByPrefix.set(prefix, minCount);
66
+ }
67
+ }
20
68
  /**
21
69
  * Get or assign a placeholder for an entity
22
70
  */
@@ -26,14 +74,55 @@ class ReplacementMap {
26
74
  return this.forward.get(originalValue);
27
75
  }
28
76
  const prefix = customPrefix || types_1.DEFAULT_PREFIXES[entityType] || 'MASKED_UNKNOWN';
29
- // Assign incrementing ID so the LLM can distinguish between entities
30
- const count = (this.counters.get(entityType) || 0) + 1;
31
- this.counters.set(entityType, count);
32
- const placeholder = `${prefix}_${count}`;
77
+ // URL origins get a URL-SHAPED placeholder (e.g. `http://masked-url-12345678.invalid`)
78
+ // instead of a bare token: the model can then legitimately append paths and query
79
+ // parameters, and the composed URL still unmasks (the placeholder origin is replaced
80
+ // by the real origin wherever it appears). `.invalid` is an RFC 2606 reserved TLD,
81
+ // so an unresolved placeholder can never route traffic anywhere.
82
+ const isUrl = prefix === 'MASKED_URL';
83
+ const urlScheme = isUrl ? (originalValue.match(/^(?:https?|wss?|ftps?):\/\//)?.[0] ?? '') : '';
84
+ const render = (id) => isUrl ? `${urlScheme}masked-url-${id}.invalid` : `${prefix}_${id}`;
85
+ let placeholder;
86
+ if (this.method === 'pseudonymization') {
87
+ // Content-derived stable id: same value → same placeholder in every request.
88
+ const digest = crypto_1.default.createHash('sha256').update(originalValue, 'utf8').digest('hex');
89
+ let level = 0;
90
+ placeholder = render(idFromDigest(digest, level));
91
+ // In-request collision probing: two DIFFERENT values sharing an id widen it
92
+ // deterministically (each value always follows the same ladder of candidates,
93
+ // so the resolution itself is stable across requests).
94
+ while (this.reverse.has(placeholder) && this.reverse.get(placeholder) !== originalValue) {
95
+ level += 1;
96
+ if (12 + 4 * level > digest.length) {
97
+ // Full-digest collision is not realistically reachable; guard anyway.
98
+ placeholder = render(`${idFromDigest(digest, level)}${this.forward.size}`);
99
+ break;
100
+ }
101
+ placeholder = render(idFromDigest(digest, level));
102
+ }
103
+ }
104
+ else {
105
+ // Anonymization: per-request incrementing id so the LLM can distinguish entities
106
+ // without the token being linkable across requests. Never collide with residue
107
+ // numbers already present in the request input.
108
+ const minForPrefix = this.minCountersByPrefix.get(prefix) || 0;
109
+ const count = Math.max(this.counters.get(entityType) || 0, minForPrefix) + 1;
110
+ this.counters.set(entityType, count);
111
+ placeholder = render(String(count));
112
+ }
33
113
  this.forward.set(originalValue, placeholder);
34
114
  if (this.method === 'pseudonymization') {
35
115
  // Only store reverse mapping for pseudonymization (enables unmasking)
36
116
  this.reverse.set(placeholder, originalValue);
117
+ // Scheme-less alias for URL placeholders: if the model reproduces the masked
118
+ // host without (or with a different) scheme, the bare form still unmasks to
119
+ // the bare origin, keeping whatever scheme the model chose.
120
+ if (isUrl && urlScheme) {
121
+ const bareKey = placeholder.slice(urlScheme.length);
122
+ if (!this.reverse.has(bareKey)) {
123
+ this.reverse.set(bareKey, originalValue.slice(urlScheme.length));
124
+ }
125
+ }
37
126
  }
38
127
  return placeholder;
39
128
  }
@@ -6,6 +6,28 @@
6
6
  */
7
7
  import { EntityMatch, MaskingConfig } from './types';
8
8
  import { ReplacementMap } from './replacementMap';
9
+ /**
10
+ * Minimum length of a detected value for it to be propagated (masked at ALL its
11
+ * exact occurrences across the request). Short values are masked only where a
12
+ * detector actually fired — propagating them risks carpet-masking common strings.
13
+ */
14
+ export declare const MIN_PROPAGATION_LENGTH = 12;
15
+ /**
16
+ * Propagate already-minted masks to EVERY exact occurrence of each detected value
17
+ * across the request's string nodes — including places no detector fired (e.g. a
18
+ * secret that was caught as `token=<secret>` in one node but rode through as
19
+ * `Authorization: Bearer <secret>` in another). This is the primary security fix:
20
+ * one value → one token → masked everywhere.
21
+ *
22
+ * Operates on parsed string nodes (system + messages subtree), so JSON-escaping in
23
+ * embedded tool_result strings is a non-issue. For values containing `"` or `\`, the
24
+ * JSON-escaped form is also searched (an identical secret embedded inside a
25
+ * JSON-in-string blob is escaped there). Mutates `body` in place; returns the number
26
+ * of occurrences replaced.
27
+ */
28
+ export declare function propagateMaskedValues(body: any, map: ReplacementMap, opts?: {
29
+ minLength?: number;
30
+ }): number;
9
31
  /**
10
32
  * Replace all detected entities in text with their placeholders
11
33
  * Returns the masked text
@@ -6,10 +6,87 @@
6
6
  * Processes matches in reverse order of position to preserve character offsets.
7
7
  */
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.MIN_PROPAGATION_LENGTH = void 0;
10
+ exports.propagateMaskedValues = propagateMaskedValues;
9
11
  exports.replaceEntities = replaceEntities;
10
12
  exports.maskJsonValue = maskJsonValue;
11
13
  const fabricatedData_1 = require("./fabricatedData");
12
14
  const detectors_1 = require("./detectors");
15
+ /**
16
+ * Minimum length of a detected value for it to be propagated (masked at ALL its
17
+ * exact occurrences across the request). Short values are masked only where a
18
+ * detector actually fired — propagating them risks carpet-masking common strings.
19
+ */
20
+ exports.MIN_PROPAGATION_LENGTH = 12;
21
+ const REGEX_META = /[.*+?^${}()|[\]\\]/g;
22
+ const WORD_CHAR = /[A-Za-z0-9_]/;
23
+ /**
24
+ * Propagate already-minted masks to EVERY exact occurrence of each detected value
25
+ * across the request's string nodes — including places no detector fired (e.g. a
26
+ * secret that was caught as `token=<secret>` in one node but rode through as
27
+ * `Authorization: Bearer <secret>` in another). This is the primary security fix:
28
+ * one value → one token → masked everywhere.
29
+ *
30
+ * Operates on parsed string nodes (system + messages subtree), so JSON-escaping in
31
+ * embedded tool_result strings is a non-issue. For values containing `"` or `\`, the
32
+ * JSON-escaped form is also searched (an identical secret embedded inside a
33
+ * JSON-in-string blob is escaped there). Mutates `body` in place; returns the number
34
+ * of occurrences replaced.
35
+ */
36
+ function propagateMaskedValues(body, map, opts = {}) {
37
+ const minLength = opts.minLength ?? exports.MIN_PROPAGATION_LENGTH;
38
+ // Build (needle → placeholder) pairs. Include the JSON-escaped form of any value
39
+ // containing escapable characters so it also matches inside JSON-in-string blobs.
40
+ const needles = [];
41
+ for (const [value, placeholder] of map.forward) {
42
+ if (typeof value !== 'string' || value.length < minLength)
43
+ continue;
44
+ needles.push({ text: value, placeholder });
45
+ const escaped = JSON.stringify(value).slice(1, -1);
46
+ if (escaped !== value && escaped.length >= minLength) {
47
+ needles.push({ text: escaped, placeholder });
48
+ }
49
+ }
50
+ if (needles.length === 0)
51
+ return 0;
52
+ // Longest needle first so a value that is a substring of another is not shadowed.
53
+ needles.sort((a, b) => b.text.length - a.text.length);
54
+ const byText = new Map(needles.map(n => [n.text, n.placeholder]));
55
+ const alternation = new RegExp(needles.map(n => n.text.replace(REGEX_META, '\\$&')).join('|'), 'g');
56
+ let replaced = 0;
57
+ const replaceInString = (s) => s.replace(alternation, (matchStr, offset, whole) => {
58
+ // Word-boundary guard: don't fire mid-identifier when the value's own edge is
59
+ // a word char and the adjacent text char is too (would corrupt an identifier).
60
+ const before = whole[offset - 1];
61
+ const after = whole[offset + matchStr.length];
62
+ if (WORD_CHAR.test(matchStr[0]) && before !== undefined && WORD_CHAR.test(before))
63
+ return matchStr;
64
+ if (WORD_CHAR.test(matchStr[matchStr.length - 1]) && after !== undefined && WORD_CHAR.test(after))
65
+ return matchStr;
66
+ replaced++;
67
+ return byText.get(matchStr) ?? matchStr;
68
+ });
69
+ const walk = (value) => {
70
+ if (typeof value === 'string')
71
+ return replaceInString(value);
72
+ if (Array.isArray(value)) {
73
+ for (let i = 0; i < value.length; i++)
74
+ value[i] = walk(value[i]);
75
+ return value;
76
+ }
77
+ if (value && typeof value === 'object') {
78
+ for (const k of Object.keys(value))
79
+ value[k] = walk(value[k]);
80
+ return value;
81
+ }
82
+ return value;
83
+ };
84
+ if (body?.system !== undefined)
85
+ body.system = walk(body.system);
86
+ if (Array.isArray(body?.messages))
87
+ walk(body.messages);
88
+ return replaced;
89
+ }
13
90
  /**
14
91
  * Get the replacement strategy for an entity type from config
15
92
  */
@@ -39,8 +39,16 @@ class StreamUnmaskBuffer {
39
39
  this.maxPrefixLength = p.length;
40
40
  }
41
41
  }
42
- // Add some buffer for the digits after the prefix
43
- this.maxPrefixLength += 5; // e.g., "_123" at most
42
+ // Add room for the id after the prefix: hash-stable ids are 8 digits
43
+ // (up to a few more under collision probing); legacy numeric ids are shorter.
44
+ this.maxPrefixLength += 16;
45
+ // Retention must also cover the longest exact key (URL-shaped placeholders like
46
+ // "https://masked-url-12345678.invalid" carry no MASKED_ prefix family).
47
+ for (const placeholder of map.reverse.keys()) {
48
+ if (placeholder.length > this.maxPrefixLength) {
49
+ this.maxPrefixLength = placeholder.length;
50
+ }
51
+ }
44
52
  // Build regex for complete placeholders
45
53
  const placeholders = Array.from(map.reverse.keys())
46
54
  .sort((a, b) => b.length - a.length);
@@ -57,13 +65,19 @@ class StreamUnmaskBuffer {
57
65
  */
58
66
  append(text) {
59
67
  this.buffer += text;
60
- // Unmask any complete placeholders in buffer
61
- this.placeholderRegex.lastIndex = 0;
62
- this.buffer = this.buffer.replace(this.placeholderRegex, (match) => this.reverseMap.get(match) || match);
63
- // Find safe flush point
68
+ // Find the safe flush point on the RAW buffer first, then unmask only the
69
+ // flushed slice. Replacing before choosing the flush point corrupted longer
70
+ // placeholders split at a digit boundary: with both MASKED_PERSON_11 and
71
+ // MASKED_PERSON_113 in the map, a chunk ending in '…MASKED_PERSON_11'
72
+ // followed by '3…' must NOT be eagerly replaced as _11 plus a stray '3'.
73
+ // couldBePartialPlaceholder retains a complete placeholder that is a strict
74
+ // prefix of a longer one, so the ambiguous tail stays buffered until the
75
+ // next chunk (or flush) disambiguates it.
64
76
  const safePoint = this.findSafeFlushPoint();
65
- const flushed = this.buffer.slice(0, safePoint);
77
+ let flushed = this.buffer.slice(0, safePoint);
66
78
  this.buffer = this.buffer.slice(safePoint);
79
+ this.placeholderRegex.lastIndex = 0;
80
+ flushed = flushed.replace(this.placeholderRegex, (match) => this.reverseMap.get(match) || match);
67
81
  return flushed;
68
82
  }
69
83
  /**
@@ -81,7 +95,11 @@ class StreamUnmaskBuffer {
81
95
  * Any suffix that could be the beginning of a known placeholder must be retained.
82
96
  */
83
97
  findSafeFlushPoint() {
84
- if (this.knownPrefixes.length === 0)
98
+ // No placeholders at all → nothing can ever match, flush everything.
99
+ // (knownPrefixes alone is not sufficient: URL-shaped placeholders like
100
+ // "https://masked-url-<id>.invalid" have no MASKED_*_ prefix family but
101
+ // still require suffix retention via the exact-key check below.)
102
+ if (this.knownPrefixes.length === 0 && this.reverseMap.size === 0)
85
103
  return this.buffer.length;
86
104
  const maxCheck = Math.min(this.buffer.length, this.maxPrefixLength);
87
105
  for (let len = maxCheck; len >= 1; len--) {
@@ -49,6 +49,9 @@ const logger_1 = require("@libs/logger");
49
49
  const logger = (0, logger_1.getDefaultLogger)();
50
50
  const rateLimitManager_1 = __importDefault(require("./rateLimitManager"));
51
51
  const usageTracker_1 = require("../utils/usageTracker");
52
+ const betaFeatureFilter_1 = require("../utils/betaFeatureFilter");
53
+ const betaFlagQuarantine = __importStar(require("./betaFlagQuarantine"));
54
+ const upstreamErrorBody_1 = require("../utils/upstreamErrorBody");
52
55
  /**
53
56
  * Process AWS Bedrock requests with payload transformation and routing
54
57
  * @param options - Request processing options
@@ -92,39 +95,42 @@ async function processBedrockRequest(options) {
92
95
  if (processedRequestBody.anthropic_version === undefined && isInvokeSubpath) {
93
96
  processedRequestBody.anthropic_version = modelDetails.anthropic_version;
94
97
  }
95
- // Add anthropic_beta from header if present (for beta features like extended thinking)
96
- const anthropicBetaHeader = options.headers['anthropic-beta'];
97
- if (anthropicBetaHeader && isInvokeSubpath) {
98
- // Bedrock expects anthropic_beta as an array of strings
99
- let betaFeatures = typeof anthropicBetaHeader === 'string'
100
- ? anthropicBetaHeader.split(',').map(s => s.trim())
101
- : Array.isArray(anthropicBetaHeader) ? anthropicBetaHeader : [];
102
- // Filter out unsupported beta headers based on configuration
103
- const excludedBetaHeaders = configService_1.default.getExcludedBetaHeaders();
104
- if (excludedBetaHeaders.length > 0) {
105
- const originalCount = betaFeatures.length;
106
- betaFeatures = betaFeatures.filter(feature => !excludedBetaHeaders.includes(feature));
107
- if (betaFeatures.length !== originalCount) {
108
- const filtered = originalCount - betaFeatures.length;
109
- logger.debug('AwsBedrockService', `Filtered ${filtered} unsupported beta feature(s): ${excludedBetaHeaders.filter(h => !betaFeatures.includes(h)).join(', ')}`);
110
- }
98
+ // Assemble anthropic_beta for invoke subpaths from all sources, then filter
99
+ // through the configured allowlist (supported_beta_headers) and denylist
100
+ // (excluded_beta_headers) so no unsupported flag can reach SAP AI Core —
101
+ // including flags injected via inject_beta_features or supplied in the body.
102
+ if (isInvokeSubpath) {
103
+ const headerFeatures = (0, betaFeatureFilter_1.parseAnthropicBetaHeader)(options.headers['anthropic-beta']);
104
+ // Header takes precedence over a body-supplied anthropic_beta (legacy behavior);
105
+ // body flags are now filtered too instead of passing through untouched.
106
+ const bodyFeatures = headerFeatures.length === 0 && Array.isArray(processedRequestBody.anthropic_beta)
107
+ ? processedRequestBody.anthropic_beta.map(String)
108
+ : [];
109
+ const injectedFeatures = Array.isArray(modelDetails.inject_beta_features)
110
+ ? modelDetails.inject_beta_features.map(String)
111
+ : [];
112
+ const candidates = (0, betaFeatureFilter_1.mergeBetaFeatures)(headerFeatures, bodyFeatures, injectedFeatures);
113
+ const filterOptions = {
114
+ supported: configService_1.default.getSupportedBetaHeaders(),
115
+ // Config denylist plus flags quarantined at runtime after upstream
116
+ // "invalid beta flag" 400s for this model (see betaFlagQuarantine).
117
+ excluded: (0, betaFeatureFilter_1.mergeBetaFeatures)(configService_1.default.getExcludedBetaHeaders(), betaFlagQuarantine.getQuarantinedFlags(modelId))
118
+ };
119
+ const betaFeatures = (0, betaFeatureFilter_1.filterBetaFeatures)(candidates, filterOptions);
120
+ const dropped = candidates.filter(feature => !betaFeatures.includes(feature));
121
+ if (dropped.length > 0) {
122
+ logger.debug('AwsBedrockService', `Filtered ${dropped.length} unsupported beta feature(s): ${dropped.join(', ')}`);
123
+ }
124
+ if (injectedFeatures.length > 0) {
125
+ logger.debug('AwsBedrockService', `Injected model-specific beta features: ${injectedFeatures.join(', ')}`);
111
126
  }
112
127
  if (betaFeatures.length > 0) {
113
128
  processedRequestBody.anthropic_beta = betaFeatures;
114
129
  logger.debug('AwsBedrockService', `Adding anthropic_beta features: ${betaFeatures.join(', ')}`);
115
130
  }
116
- }
117
- // Inject model-specific beta features from configuration (e.g., context-1m for Opus/Sonnet 4.6)
118
- if (isInvokeSubpath && modelDetails.inject_beta_features && Array.isArray(modelDetails.inject_beta_features)) {
119
- if (!processedRequestBody.anthropic_beta) {
120
- processedRequestBody.anthropic_beta = [];
121
- }
122
- for (const feature of modelDetails.inject_beta_features) {
123
- if (!processedRequestBody.anthropic_beta.includes(feature)) {
124
- processedRequestBody.anthropic_beta.push(feature);
125
- }
131
+ else {
132
+ delete processedRequestBody.anthropic_beta;
126
133
  }
127
- logger.debug('AwsBedrockService', `Injected model-specific beta features: ${modelDetails.inject_beta_features.join(', ')}`);
128
134
  }
129
135
  if (isInvokeSubpath && isNativeSubpath) {
130
136
  // Check if model explicitly disables prompt caching - if so, filter out cache_control parameters
@@ -320,7 +326,20 @@ async function handleNativeSubpath(options) {
320
326
  catch (error) {
321
327
  logger.error('AwsBedrockService', `Error in native request: ${error.message}`);
322
328
  if (error.response) {
323
- logger.error('AwsBedrockService', `SAP AI Core error response: ${error.response.status}`, error.response.data);
329
+ logger.error('AwsBedrockService', `SAP AI Core error response: ${error.response.status}`, undefined, { data: error.response.data });
330
+ if (debugRequestId) {
331
+ payloadLogger.savePayload(debugRequestId, '03_native_error_from_sap', {
332
+ status: error.response.status,
333
+ data: error.response.data
334
+ }, req, res);
335
+ }
336
+ const sentBetaFlags = Array.isArray(requestBody?.anthropic_beta)
337
+ ? requestBody.anthropic_beta.map(String)
338
+ : [];
339
+ if (sentBetaFlags.length > 0
340
+ && betaFlagQuarantine.isInvalidBetaFlagError(error.response.status, error.response.data)) {
341
+ betaFlagQuarantine.recordBetaFlagRejection(modelId, error.response.data, sentBetaFlags);
342
+ }
324
343
  }
325
344
  throw error;
326
345
  }
@@ -412,7 +431,21 @@ async function handleEmulatedSubpath(options) {
412
431
  isTimeout: error.isAxiosError && error.code === 'ECONNABORTED'
413
432
  });
414
433
  if (error.response) {
415
- logger.error('AwsBedrockService', `SAP AI Core error response: ${error.response.status}`, error.response.data);
434
+ logger.error('AwsBedrockService', `SAP AI Core error response: ${error.response.status}`, undefined, { data: error.response.data });
435
+ if (debugRequestId) {
436
+ payloadLogger.savePayload(debugRequestId, '03_emulated_error_from_sap', {
437
+ status: error.response.status,
438
+ data: error.response.data
439
+ }, req, res);
440
+ }
441
+ // Converse payloads carry no anthropic_beta today — this hook is defense-in-depth should the transform ever forward beta flags.
442
+ const sentBetaFlags = Array.isArray(requestBody?.anthropic_beta)
443
+ ? requestBody.anthropic_beta.map(String)
444
+ : [];
445
+ if (sentBetaFlags.length > 0
446
+ && betaFlagQuarantine.isInvalidBetaFlagError(error.response.status, error.response.data)) {
447
+ betaFlagQuarantine.recordBetaFlagRejection(modelId, error.response.data, sentBetaFlags);
448
+ }
416
449
  }
417
450
  throw error;
418
451
  }
@@ -926,14 +959,32 @@ async function handleNativeStreamingRequest(options) {
926
959
  code: error.code,
927
960
  isTimeout: error.isAxiosError && error.code === 'ECONNABORTED'
928
961
  });
929
- // Enhanced error logging for auth issues
930
- if (error.response && error.response.status === 401) {
931
- logger.error('AwsBedrockService', `Authentication failed (401) for request to: ${targetUrl}`);
932
- logger.error('AwsBedrockService', 'Error details:', undefined, { data: error.response.data });
933
- // Log auth details if DEBUG is enabled
934
- if (process.env.DEBUG === 'true') {
935
- logger.debug('AwsBedrockService', 'DEBUG - Auth token used:', { token: authToken });
936
- logger.debug('AwsBedrockService', 'DEBUG - Response headers: ' + (0, logger_1.createHeadersPreview)(error.response.headers));
962
+ if (error.response) {
963
+ // responseType:'stream' delivers the error body as a Readable — drain it so
964
+ // SAP's actual error is loggable, persisted, and quarantine-matchable.
965
+ const upstreamErrorBody = await (0, upstreamErrorBody_1.readUpstreamErrorBody)(error.response);
966
+ logger.error('AwsBedrockService', `SAP AI Core error response: ${error.response.status}`, undefined, { data: upstreamErrorBody });
967
+ if (debugRequestId) {
968
+ payloadLogger.savePayload(debugRequestId, '03_native_streaming_error_from_sap', {
969
+ status: error.response.status,
970
+ data: upstreamErrorBody
971
+ }, req, res);
972
+ }
973
+ // Enhanced error logging for auth issues
974
+ if (error.response.status === 401) {
975
+ logger.error('AwsBedrockService', `Authentication failed (401) for request to: ${targetUrl}`);
976
+ // Log auth details if DEBUG is enabled
977
+ if (process.env.DEBUG === 'true') {
978
+ logger.debug('AwsBedrockService', 'DEBUG - Auth token used:', { token: authToken });
979
+ logger.debug('AwsBedrockService', 'DEBUG - Response headers: ' + (0, logger_1.createHeadersPreview)(error.response.headers));
980
+ }
981
+ }
982
+ const sentBetaFlags = Array.isArray(requestBody?.anthropic_beta)
983
+ ? requestBody.anthropic_beta.map(String)
984
+ : [];
985
+ if (sentBetaFlags.length > 0
986
+ && betaFlagQuarantine.isInvalidBetaFlagError(error.response.status, upstreamErrorBody)) {
987
+ betaFlagQuarantine.recordBetaFlagRejection(modelId, upstreamErrorBody, sentBetaFlags);
937
988
  }
938
989
  }
939
990
  if (!res.writableEnded) {
@@ -1300,18 +1351,39 @@ async function handleEmulatedStreamingRequest(options) {
1300
1351
  code: error.code,
1301
1352
  isTimeout: error.isAxiosError && error.code === 'ECONNABORTED'
1302
1353
  });
1303
- // Enhanced error logging for auth issues
1304
- if (error.response && error.response.status === 401) {
1305
- logger.error('AwsBedrockService', `Authentication failed (401) for request to: ${targetUrl}`);
1306
- logger.error('AwsBedrockService', 'Error details:', undefined, { data: error.response.data });
1307
- // Log auth details if DEBUG is enabled
1308
- if (process.env.DEBUG === 'true') {
1309
- logger.debug('AwsBedrockService', 'DEBUG - Auth token used:', { token: authToken });
1310
- logger.debug('AwsBedrockService', 'DEBUG - Response headers: ' + (0, logger_1.createHeadersPreview)(error.response.headers));
1354
+ let upstreamErrorBody;
1355
+ if (error.response) {
1356
+ // responseType:'stream' delivers the error body as a Readable — drain it so
1357
+ // SAP's actual error is loggable, persisted, and quarantine-matchable.
1358
+ upstreamErrorBody = await (0, upstreamErrorBody_1.readUpstreamErrorBody)(error.response);
1359
+ logger.error('AwsBedrockService', `SAP AI Core error response: ${error.response.status}`, undefined, { data: upstreamErrorBody });
1360
+ if (debugRequestId) {
1361
+ payloadLogger.savePayload(debugRequestId, '03_emulated_streaming_error_from_sap', {
1362
+ status: error.response.status,
1363
+ data: upstreamErrorBody
1364
+ }, req, res);
1365
+ }
1366
+ // Enhanced error logging for auth issues
1367
+ if (error.response.status === 401) {
1368
+ logger.error('AwsBedrockService', `Authentication failed (401) for request to: ${targetUrl}`);
1369
+ // Log auth details if DEBUG is enabled
1370
+ if (process.env.DEBUG === 'true') {
1371
+ logger.debug('AwsBedrockService', 'DEBUG - Auth token used:', { token: authToken });
1372
+ logger.debug('AwsBedrockService', 'DEBUG - Response headers: ' + (0, logger_1.createHeadersPreview)(error.response.headers));
1373
+ }
1374
+ }
1375
+ // Converse payloads carry no anthropic_beta today — this hook is defense-in-depth should the transform ever forward beta flags.
1376
+ const sentBetaFlags = Array.isArray(requestBody?.anthropic_beta)
1377
+ ? requestBody.anthropic_beta.map(String)
1378
+ : [];
1379
+ if (sentBetaFlags.length > 0
1380
+ && betaFlagQuarantine.isInvalidBetaFlagError(error.response.status, upstreamErrorBody)) {
1381
+ betaFlagQuarantine.recordBetaFlagRejection(modelId, upstreamErrorBody, sentBetaFlags);
1311
1382
  }
1312
1383
  }
1313
1384
  if (!res.writableEnded) {
1314
- const message = error.response?.data?.message || error.message || "Failed to establish stream";
1385
+ const message = upstreamErrorBody?.error?.message || upstreamErrorBody?.message
1386
+ || error.message || "Failed to establish stream";
1315
1387
  const status = error.response?.status || 500;
1316
1388
  const errToSend = new Error(message);
1317
1389
  errToSend.status = status;
@@ -0,0 +1,18 @@
1
+ export declare function getQuarantinedFlags(modelId: string): string[];
2
+ export declare function quarantineFlags(modelId: string, flags: string[]): void;
3
+ export declare function clearQuarantine(): void;
4
+ /**
5
+ * Whether an upstream error response is a beta-flag rejection.
6
+ */
7
+ export declare function isInvalidBetaFlagError(status: number | undefined, errorData: any): boolean;
8
+ /**
9
+ * Flags to quarantine: those explicitly named in the error body when present
10
+ * (first-party-style errors name them), else all flags that were sent
11
+ * (Bedrock's bare "invalid beta flag" names none).
12
+ */
13
+ export declare function resolveRejectedFlags(errorData: any, sentFlags: string[]): string[];
14
+ /**
15
+ * Record a beta-flag rejection so subsequent requests for this model omit
16
+ * the offending flags. Returns the quarantined flags.
17
+ */
18
+ export declare function recordBetaFlagRejection(modelId: string, errorData: any, sentFlags: string[]): string[];