cartethyia 2.6.7 → 2.7.0

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 (187) hide show
  1. package/.env.example +106 -113
  2. package/dist/dashboard/assets/{BannedPage-Dskhyp0l.js → BannedPage-P76QLGJA.js} +1 -1
  3. package/dist/dashboard/assets/BannedPage-P76QLGJA.js.gz +0 -0
  4. package/dist/dashboard/assets/{CliToolDetailPage--SJhk7qQ.js → CliToolDetailPage-DdBggJS9.js} +1 -1
  5. package/dist/dashboard/assets/CliToolDetailPage-DdBggJS9.js.gz +0 -0
  6. package/dist/dashboard/assets/{CliToolsPage-C203VT-c.js → CliToolsPage-aN1iXhhP.js} +1 -1
  7. package/dist/dashboard/assets/CliToolsPage-aN1iXhhP.js.gz +0 -0
  8. package/dist/dashboard/assets/{CombosPage-BcS7kGG1.js → CombosPage-DE-F1nxZ.js} +1 -1
  9. package/dist/dashboard/assets/CombosPage-DE-F1nxZ.js.gz +0 -0
  10. package/dist/dashboard/assets/{ConsoleLogPage-ChmiSLwO.js → ConsoleLogPage-5SpmfLyw.js} +1 -1
  11. package/dist/dashboard/assets/ConsoleLogPage-5SpmfLyw.js.gz +0 -0
  12. package/dist/dashboard/assets/{CustomizationPage-uujbBYwC.js → CustomizationPage-3CUxMy2X.js} +1 -1
  13. package/dist/dashboard/assets/CustomizationPage-3CUxMy2X.js.gz +0 -0
  14. package/dist/dashboard/assets/{HealthEventsModal-fKAWd-OQ.js → HealthEventsModal-BkmJQKC0.js} +1 -1
  15. package/dist/dashboard/assets/HealthEventsModal-BkmJQKC0.js.gz +0 -0
  16. package/dist/dashboard/assets/{LoginPage-DoOUthbG.js → LoginPage-B_0FUKWc.js} +1 -1
  17. package/dist/dashboard/assets/LoginPage-B_0FUKWc.js.gz +0 -0
  18. package/dist/dashboard/assets/{ModelPicker-CA1kLs45.js → ModelPicker-DKVslqG3.js} +1 -1
  19. package/dist/dashboard/assets/ModelPicker-DKVslqG3.js.gz +0 -0
  20. package/dist/dashboard/assets/OverviewPage-D-H4MEiT.js +1 -0
  21. package/dist/dashboard/assets/OverviewPage-D-H4MEiT.js.gz +0 -0
  22. package/dist/dashboard/assets/{ProviderDetailPage-CFu1qGcT.js → ProviderDetailPage-CEJw6Guo.js} +3 -3
  23. package/dist/dashboard/assets/ProviderDetailPage-CEJw6Guo.js.gz +0 -0
  24. package/dist/dashboard/assets/{ProvidersPage-CnlJZbxF.js → ProvidersPage-CEplAR-i.js} +1 -1
  25. package/dist/dashboard/assets/ProvidersPage-CEplAR-i.js.gz +0 -0
  26. package/dist/dashboard/assets/{ProxyPage-B9mBTweh.js → ProxyPage-CAN7hy0f.js} +1 -1
  27. package/dist/dashboard/assets/ProxyPage-CAN7hy0f.js.gz +0 -0
  28. package/dist/dashboard/assets/{QuotaPage-BueSWX5D.js → QuotaPage-BjldQwIU.js} +1 -1
  29. package/dist/dashboard/assets/QuotaPage-BjldQwIU.js.gz +0 -0
  30. package/dist/dashboard/assets/SettingsPage-DUIoLRxk.js +1 -0
  31. package/dist/dashboard/assets/SettingsPage-DUIoLRxk.js.gz +0 -0
  32. package/dist/dashboard/assets/{SetupPage-DQ9oDRpi.js → SetupPage-_bCaTM0v.js} +1 -1
  33. package/dist/dashboard/assets/SetupPage-_bCaTM0v.js.gz +0 -0
  34. package/dist/dashboard/assets/{SortableList-DrKeMcGN.js → SortableList-fI7JkdIB.js} +1 -1
  35. package/dist/dashboard/assets/SortableList-fI7JkdIB.js.gz +0 -0
  36. package/dist/dashboard/assets/{StudioPage-qjuYtccc.js → StudioPage-CHVZqskZ.js} +1 -1
  37. package/dist/dashboard/assets/StudioPage-CHVZqskZ.js.gz +0 -0
  38. package/dist/dashboard/assets/UsagePage-DiFBymHj.js +56 -0
  39. package/dist/dashboard/assets/UsagePage-DiFBymHj.js.gz +0 -0
  40. package/dist/dashboard/assets/{api-keys-C23S9sgp.js → api-keys-DFY_fZU4.js} +1 -1
  41. package/dist/dashboard/assets/api-keys-DFY_fZU4.js.gz +0 -0
  42. package/dist/dashboard/assets/app-BOJ1_xUB.css +2 -0
  43. package/dist/dashboard/assets/app-BOJ1_xUB.css.gz +0 -0
  44. package/dist/dashboard/assets/{assert-DU6qrMA_.js → assert-CwdnZARt.js} +1 -1
  45. package/dist/dashboard/assets/assert-CwdnZARt.js.gz +0 -0
  46. package/dist/dashboard/assets/{badge-Bkz_-RrM.js → badge-Dr-uf0Au.js} +1 -1
  47. package/dist/dashboard/assets/badge-Dr-uf0Au.js.gz +0 -0
  48. package/dist/dashboard/assets/{banner-C-7y3VKV.js → banner-BddmMdoS.js} +1 -1
  49. package/dist/dashboard/assets/banner-BddmMdoS.js.gz +0 -0
  50. package/dist/dashboard/assets/{card-BOPBuBzD.js → card-CK0PXWlX.js} +1 -1
  51. package/dist/dashboard/assets/card-CK0PXWlX.js.gz +0 -0
  52. package/dist/dashboard/assets/{cli-tools-BaFUuFFF.js → cli-tools-CcB6mGh2.js} +1 -1
  53. package/dist/dashboard/assets/cli-tools-CcB6mGh2.js.gz +0 -0
  54. package/dist/dashboard/assets/{clipboard-button-e-Mlk0FH.js → clipboard-button-DGb2P9Yx.js} +1 -1
  55. package/dist/dashboard/assets/clipboard-button-DGb2P9Yx.js.gz +0 -0
  56. package/dist/dashboard/assets/{dialog-Iec_ywbD.js → dialog-LkfuL50u.js} +1 -1
  57. package/dist/dashboard/assets/dialog-LkfuL50u.js.gz +0 -0
  58. package/dist/dashboard/assets/{drawer-Bs8MIIHc.js → drawer-ZbEZ_84c.js} +1 -1
  59. package/dist/dashboard/assets/drawer-ZbEZ_84c.js.gz +0 -0
  60. package/dist/dashboard/assets/{entry-DcobaKFu.js → entry-BJfsVMvA.js} +1 -1
  61. package/dist/dashboard/assets/entry-BJfsVMvA.js.gz +0 -0
  62. package/dist/dashboard/assets/{entry-DOddwoc0.js → entry-CWBY5A0f.js} +3 -3
  63. package/dist/dashboard/assets/entry-CWBY5A0f.js.gz +0 -0
  64. package/dist/dashboard/assets/{entry-BRszm4_Z.js → entry-CvC1-Zqx.js} +1 -1
  65. package/dist/dashboard/assets/entry-CvC1-Zqx.js.gz +0 -0
  66. package/dist/dashboard/assets/{github-badge-BuDsrsPn.js → github-badge-BxpznFLt.js} +1 -1
  67. package/dist/dashboard/assets/github-badge-BxpznFLt.js.gz +0 -0
  68. package/dist/dashboard/assets/{index-BeLRhsmt.js → index-CDzP3ovJ.js} +2 -2
  69. package/dist/dashboard/assets/index-CDzP3ovJ.js.gz +0 -0
  70. package/dist/dashboard/assets/{inline-DToMICwl.js → inline-Ae13MQ5w.js} +1 -1
  71. package/dist/dashboard/assets/inline-Ae13MQ5w.js.gz +0 -0
  72. package/dist/dashboard/assets/{input-WlyHWeG2.js → input-D23fo7cp.js} +1 -1
  73. package/dist/dashboard/assets/input-D23fo7cp.js.gz +0 -0
  74. package/dist/dashboard/assets/{layout-DaJ5baJr.js → layout-CSsxjj5P.js} +1 -1
  75. package/dist/dashboard/assets/layout-CSsxjj5P.js.gz +0 -0
  76. package/dist/dashboard/assets/{list-row-B3oSK13z.js → list-row-dmbwZMDY.js} +1 -1
  77. package/dist/dashboard/assets/list-row-dmbwZMDY.js.gz +0 -0
  78. package/dist/dashboard/assets/{live-ta6GMb-8.js → live-BG1oUsZZ.js} +1 -1
  79. package/dist/dashboard/assets/live-BG1oUsZZ.js.gz +0 -0
  80. package/dist/dashboard/assets/minimize-2-D-MN8W8a.js +1 -0
  81. package/dist/dashboard/assets/minimize-2-D-MN8W8a.js.gz +0 -0
  82. package/dist/dashboard/assets/{mutation-g_fduiNw.js → mutation-DSF-e4ho.js} +1 -1
  83. package/dist/dashboard/assets/mutation-DSF-e4ho.js.gz +0 -0
  84. package/dist/dashboard/assets/{numbers-Dulqgys5.js → numbers-Bm_aECY7.js} +1 -1
  85. package/dist/dashboard/assets/{numbers-Dulqgys5.js.gz → numbers-Bm_aECY7.js.gz} +0 -0
  86. package/dist/dashboard/assets/{query-keys-UETqfytp.js → query-keys-CF56nYIV.js} +1 -1
  87. package/dist/dashboard/assets/query-keys-CF56nYIV.js.gz +0 -0
  88. package/dist/dashboard/assets/{routing-BPUo-W8d.js → routing-CeA71n7G.js} +1 -1
  89. package/dist/dashboard/assets/routing-CeA71n7G.js.gz +0 -0
  90. package/dist/dashboard/assets/{select-BfZ3SCTM.js → select-KIT0rC6G.js} +1 -1
  91. package/dist/dashboard/assets/select-KIT0rC6G.js.gz +0 -0
  92. package/dist/dashboard/assets/settings-B_2gkePf.js +1 -0
  93. package/dist/dashboard/assets/settings-B_2gkePf.js.gz +0 -0
  94. package/dist/dashboard/assets/{stack-B1w5BM0u.js → stack-D7WOSzcX.js} +1 -1
  95. package/dist/dashboard/assets/stack-D7WOSzcX.js.gz +0 -0
  96. package/dist/dashboard/assets/{state-DhZBfWgx.js → state-BBxtKZOQ.js} +1 -1
  97. package/dist/dashboard/assets/state-BBxtKZOQ.js.gz +0 -0
  98. package/dist/dashboard/assets/{switch-yD7D99Lg.js → switch-DNS_D5Ea.js} +1 -1
  99. package/dist/dashboard/assets/switch-DNS_D5Ea.js.gz +0 -0
  100. package/dist/dashboard/assets/{useMutation-EDFRkn1i.js → useMutation-BgoxdoMk.js} +1 -1
  101. package/dist/dashboard/assets/useMutation-BgoxdoMk.js.gz +0 -0
  102. package/dist/dashboard/index.html +1 -1
  103. package/dist/dashboard/index.html.gz +0 -0
  104. package/dist/dashboard/precompressed-assets.json +51 -51
  105. package/package.json +96 -96
  106. package/src/boot/app.ts +714 -722
  107. package/src/boot/dependencies.ts +729 -750
  108. package/src/config/resources.ts +70 -137
  109. package/src/config/security.ts +37 -73
  110. package/src/config/spec.ts +358 -378
  111. package/src/console/console-router.ts +225 -233
  112. package/src/console/domain-registration.ts +381 -391
  113. package/src/console/remote-support/contracts.ts +3 -1
  114. package/src/console/remote-support/service.ts +115 -6
  115. package/src/main.ts +167 -169
  116. package/src/observability/metrics.ts +467 -457
  117. package/src/observability/runtime-metrics.ts +269 -254
  118. package/src/protocol/gateway-error.ts +232 -233
  119. package/src/providers/integrations/commandcode/commandcode.ts +134 -104
  120. package/src/providers/integrations/commandcode/discovery.ts +6 -5
  121. package/src/providers/integrations/qoder/qoder.ts +597 -23
  122. package/src/security/abuse.ts +17 -0
  123. package/src/transport/guards/auth-guard.ts +109 -127
  124. package/src/transport/guards/pipeline.ts +220 -226
  125. package/src/transport/guards/request-context.ts +238 -318
  126. package/src/transport/request/inflight.ts +21 -2
  127. package/dist/dashboard/assets/BannedPage-Dskhyp0l.js.gz +0 -0
  128. package/dist/dashboard/assets/CliToolDetailPage--SJhk7qQ.js.gz +0 -0
  129. package/dist/dashboard/assets/CliToolsPage-C203VT-c.js.gz +0 -0
  130. package/dist/dashboard/assets/CombosPage-BcS7kGG1.js.gz +0 -0
  131. package/dist/dashboard/assets/ConsoleLogPage-ChmiSLwO.js.gz +0 -0
  132. package/dist/dashboard/assets/CustomizationPage-uujbBYwC.js.gz +0 -0
  133. package/dist/dashboard/assets/HealthEventsModal-fKAWd-OQ.js.gz +0 -0
  134. package/dist/dashboard/assets/LoginPage-DoOUthbG.js.gz +0 -0
  135. package/dist/dashboard/assets/ModelPicker-CA1kLs45.js.gz +0 -0
  136. package/dist/dashboard/assets/OverviewPage-C5Z-UBz1.js +0 -1
  137. package/dist/dashboard/assets/OverviewPage-C5Z-UBz1.js.gz +0 -0
  138. package/dist/dashboard/assets/ProviderDetailPage-CFu1qGcT.js.gz +0 -0
  139. package/dist/dashboard/assets/ProvidersPage-CnlJZbxF.js.gz +0 -0
  140. package/dist/dashboard/assets/ProxyPage-B9mBTweh.js.gz +0 -0
  141. package/dist/dashboard/assets/QuotaPage-BueSWX5D.js.gz +0 -0
  142. package/dist/dashboard/assets/SettingsPage-B0x00MVY.js +0 -1
  143. package/dist/dashboard/assets/SettingsPage-B0x00MVY.js.gz +0 -0
  144. package/dist/dashboard/assets/SetupPage-DQ9oDRpi.js.gz +0 -0
  145. package/dist/dashboard/assets/SortableList-DrKeMcGN.js.gz +0 -0
  146. package/dist/dashboard/assets/StudioPage-qjuYtccc.js.gz +0 -0
  147. package/dist/dashboard/assets/UsagePage-Be7aNviQ.js +0 -56
  148. package/dist/dashboard/assets/UsagePage-Be7aNviQ.js.gz +0 -0
  149. package/dist/dashboard/assets/api-keys-C23S9sgp.js.gz +0 -0
  150. package/dist/dashboard/assets/app-BUSiYL6F.css +0 -2
  151. package/dist/dashboard/assets/app-BUSiYL6F.css.gz +0 -0
  152. package/dist/dashboard/assets/assert-DU6qrMA_.js.gz +0 -0
  153. package/dist/dashboard/assets/badge-Bkz_-RrM.js.gz +0 -0
  154. package/dist/dashboard/assets/banner-C-7y3VKV.js.gz +0 -0
  155. package/dist/dashboard/assets/card-BOPBuBzD.js.gz +0 -0
  156. package/dist/dashboard/assets/cli-tools-BaFUuFFF.js.gz +0 -0
  157. package/dist/dashboard/assets/clipboard-button-e-Mlk0FH.js.gz +0 -0
  158. package/dist/dashboard/assets/dialog-Iec_ywbD.js.gz +0 -0
  159. package/dist/dashboard/assets/drawer-Bs8MIIHc.js.gz +0 -0
  160. package/dist/dashboard/assets/entry-BRszm4_Z.js.gz +0 -0
  161. package/dist/dashboard/assets/entry-DOddwoc0.js.gz +0 -0
  162. package/dist/dashboard/assets/entry-DcobaKFu.js.gz +0 -0
  163. package/dist/dashboard/assets/github-badge-BuDsrsPn.js.gz +0 -0
  164. package/dist/dashboard/assets/index-BeLRhsmt.js.gz +0 -0
  165. package/dist/dashboard/assets/inline-DToMICwl.js.gz +0 -0
  166. package/dist/dashboard/assets/input-WlyHWeG2.js.gz +0 -0
  167. package/dist/dashboard/assets/layout-DaJ5baJr.js.gz +0 -0
  168. package/dist/dashboard/assets/list-row-B3oSK13z.js.gz +0 -0
  169. package/dist/dashboard/assets/live-ta6GMb-8.js.gz +0 -0
  170. package/dist/dashboard/assets/model-bans-BmYTmbAk.js +0 -1
  171. package/dist/dashboard/assets/model-bans-BmYTmbAk.js.gz +0 -0
  172. package/dist/dashboard/assets/mutation-g_fduiNw.js.gz +0 -0
  173. package/dist/dashboard/assets/query-keys-UETqfytp.js.gz +0 -0
  174. package/dist/dashboard/assets/routing-BPUo-W8d.js.gz +0 -0
  175. package/dist/dashboard/assets/select-BfZ3SCTM.js.gz +0 -0
  176. package/dist/dashboard/assets/settings--3hxSyry.js +0 -1
  177. package/dist/dashboard/assets/settings--3hxSyry.js.gz +0 -0
  178. package/dist/dashboard/assets/stack-B1w5BM0u.js.gz +0 -0
  179. package/dist/dashboard/assets/state-DhZBfWgx.js.gz +0 -0
  180. package/dist/dashboard/assets/switch-yD7D99Lg.js.gz +0 -0
  181. package/dist/dashboard/assets/useMutation-EDFRkn1i.js.gz +0 -0
  182. package/src/console/admin/model-abuse/contracts.ts +0 -38
  183. package/src/console/admin/model-abuse/routes.ts +0 -97
  184. package/src/security/model-abuse.ts +0 -704
  185. package/src/transport/guards/concurrency-guard.ts +0 -125
  186. /package/dist/dashboard/assets/{app-MdVfwraB.js → app-CaXZ9SCn.js} +0 -0
  187. /package/dist/dashboard/assets/{app-MdVfwraB.js.gz → app-CaXZ9SCn.js.gz} +0 -0
@@ -1,137 +1,70 @@
1
- /**
2
- * Canonical resource policy: how much memory this process may use, and how
3
- * much work it may hold at once.
4
- *
5
- * These two numbers were previously decided in three unrelated places — a
6
- * hardcoded cap in `concurrency-guard.ts`, a host-wide `os.totalmem()` in the
7
- * headroom guard, and a fallback constant in `runtime-metrics.ts` — which
8
- * meant a container limited to 512 MiB admitted the same concurrency as a 4 GiB
9
- * one and only started shedding once the *host* was nearly full. On a 1-core /
10
- * 2 GiB VPS that is the difference between shedding early and being
11
- * OOM-killed, and the OOM kills the dashboard too — the one surface that would
12
- * have explained what happened.
13
- *
14
- * Everything here is derived from one limit and one body ceiling, so the
15
- * numbers cannot disagree with each other:
16
- *
17
- * concurrency = floor(limit × BUDGET_FRACTION / (TRANSIENT_COPIES × body))
18
- *
19
- * The transient-copies factor is why this is not simply "limit / body": a
20
- * request body is held more than once between the socket read, the JSON parse,
21
- * and the upstream re-serialization, so budgeting one copy per request
22
- * understates the real hold by a factor of about two.
23
- */
24
- import { CONFIG_SPEC, readOptionalInt } from "./spec";
25
- import { resolveMaxBodyBytes } from "./runtime";
26
-
27
- /** Used when no limit is discoverable at all (native dev, macOS, Windows). */
28
- export const FALLBACK_MEMORY_LIMIT_BYTES = 512 * 1024 * 1024;
29
-
30
- /**
31
- * Ceiling on concurrent in-flight requests per process.
32
- *
33
- * A hard ceiling even when memory would allow more: this process is one core,
34
- * and past this point queueing delay, not memory, is what degrades. It also
35
- * bounds every per-request structure that is sized by concurrency.
36
- */
37
- export const MAX_PROCESS_CONCURRENCY = 64;
38
-
39
- /**
40
- * How many copies of a request body are resident at once.
41
- *
42
- * Measured, not assumed: the socket read buffer, the parsed object graph, and
43
- * the upstream re-serialization each hold a body-sized allocation, and garbage
44
- * collection lags rather than keeping them to one. Budgeting one copy halves
45
- * the real peak and lets the process admit twice the concurrency it can hold.
46
- */
47
- export const REQUEST_BODY_TRANSIENT_COPIES = 2;
48
-
49
- /** Share of the memory limit all in-flight request bodies together may hold. */
50
- export const REQUEST_BODY_BUDGET_FRACTION = 0.25;
51
-
52
- /** Share of the memory limit kept free for allocation spikes and GC slack. */
53
- export const MEMORY_HEADROOM_FRACTION = 0.2;
54
-
55
- /**
56
- * Concurrency this process should admit, derived from the memory limit and the
57
- * body ceiling rather than guessed.
58
- *
59
- * Worked examples, which are the point of having one formula:
60
- * - 2 GiB limit, 4 MiB bodies → 64 (the ceiling; memory is not the binder)
61
- * - 512 MiB limit, 4 MiB bodies → 16
62
- * - 256 MiB limit, 16 MiB bodies → 2
63
- *
64
- * An explicit override is honoured, clamped to 1..MAX_PROCESS_CONCURRENCY. It
65
- * is the operator's call and is never silently raised back to the derived
66
- * value; exceeding the derived figure is a deliberate choice, so it is
67
- * reported at the call site rather than corrected here.
68
- */
69
- export function deriveProcessConcurrency(options: {
70
- readonly memoryLimitBytes: number;
71
- readonly maxBodyBytes: number;
72
- readonly override?: number | undefined;
73
- }): number {
74
- if (options.override !== undefined) {
75
- if (!Number.isInteger(options.override)) return 1;
76
- return Math.min(MAX_PROCESS_CONCURRENCY, Math.max(1, options.override));
77
- }
78
- const body = Math.max(1, options.maxBodyBytes);
79
- const perRequest = REQUEST_BODY_TRANSIENT_COPIES * body;
80
- const budget = Math.max(1, Math.floor(options.memoryLimitBytes * REQUEST_BODY_BUDGET_FRACTION));
81
- const derived = Math.floor(budget / perRequest);
82
- return Math.min(MAX_PROCESS_CONCURRENCY, Math.max(1, derived));
83
- }
84
-
85
- /**
86
- * Bytes kept free below the memory limit.
87
- *
88
- * A guard that only reacts once RSS equals the limit has already lost: the
89
- * allocation that breaches it happens between two samples, and the spike that
90
- * matters — an upstream response landing all at once — arrives in far less
91
- * than one sample interval.
92
- */
93
- export function deriveMemoryHeadroomBytes(memoryLimitBytes: number): number {
94
- return Math.ceil(memoryLimitBytes * MEMORY_HEADROOM_FRACTION);
95
- }
96
-
97
- /**
98
- * The optional operator override, read through the config spec so "unset" and
99
- * "out of range" are treated the way every other knob treats them.
100
- */
101
- export function resolveProcessConcurrencyOverride(): number | undefined {
102
- return readOptionalInt(
103
- "CARTETHYIA_PROCESS_CONCURRENCY_LIMIT",
104
- CONFIG_SPEC.CARTETHYIA_PROCESS_CONCURRENCY_LIMIT,
105
- );
106
- }
107
-
108
- /**
109
- * Slack added on top of every key's concurrency limit.
110
- *
111
- * Operator configuration, not something a key carries about itself: it is
112
- * deliberate hidden allowance so a run that lands a few requests over the
113
- * number an operator chose is not refused at that number. The effective
114
- * ceiling is `limit + tolerance`, and only requests past it are refused.
115
- */
116
- export function resolveConcurrencyTolerance(): number {
117
- const value = readOptionalInt(
118
- "CARTETHYIA_CONCURRENCY_TOLERANCE",
119
- CONFIG_SPEC.CARTETHYIA_CONCURRENCY_TOLERANCE,
120
- );
121
- return value === undefined ? 0 : Math.max(0, value);
122
- }
123
-
124
- /**
125
- * The effective concurrency for this process.
126
- *
127
- * Takes the limit as an argument rather than resolving it here: the limit
128
- * owner is `resolveMemoryLimitBytes()` in `observability/runtime-metrics.ts`,
129
- * and duplicating that resolution would let the two drift.
130
- */
131
- export function resolveProcessConcurrency(memoryLimitBytes: number): number {
132
- return deriveProcessConcurrency({
133
- memoryLimitBytes,
134
- maxBodyBytes: resolveMaxBodyBytes(),
135
- override: resolveProcessConcurrencyOverride(),
136
- });
137
- }
1
+ /**
2
+ * Canonical resource policy: how much memory this process may use, and how
3
+ * much room it must keep free.
4
+ *
5
+ * The memory figure used to be decided in unrelated places — a host-wide
6
+ * `os.totalmem()` in the headroom guard, a fallback constant in
7
+ * `runtime-metrics.ts` — which meant a container limited to 512 MiB shed on
8
+ * the same threshold as a 4 GiB one and only reacted once the *host* was
9
+ * nearly full. On a 1-core / 2 GiB VPS that is the difference between
10
+ * shedding early and being OOM-killed, and the OOM kills the dashboard too —
11
+ * the one surface that would have explained what happened.
12
+ *
13
+ * How many requests may run at once is NOT decided here. That is admission's
14
+ * job: the tenant ceiling and each key's own limit, both set in Settings. A
15
+ * second, process-wide ceiling derived from memory sat below the number an
16
+ * operator could configure and refused requests that every configured limit
17
+ * still allowed, which is why it is gone.
18
+ */
19
+ import { CONFIG_SPEC, readOptionalInt } from "./spec";
20
+
21
+ /** Used when no limit is discoverable at all (native dev, macOS, Windows). */
22
+ export const FALLBACK_MEMORY_LIMIT_BYTES = 512 * 1024 * 1024;
23
+
24
+
25
+ /**
26
+ * How many copies of a request body are resident at once.
27
+ *
28
+ * Measured, not assumed: the socket read buffer, the parsed object graph, and
29
+ * the upstream re-serialization each hold a body-sized allocation, and garbage
30
+ * collection lags rather than keeping them to one. Budgeting one copy halves
31
+ * the real peak and lets the process admit twice the concurrency it can hold.
32
+ */
33
+ export const REQUEST_BODY_TRANSIENT_COPIES = 2;
34
+
35
+ /** Share of the memory limit all in-flight request bodies together may hold. */
36
+ export const REQUEST_BODY_BUDGET_FRACTION = 0.25;
37
+
38
+ /** Share of the memory limit kept free for allocation spikes and GC slack. */
39
+ export const MEMORY_HEADROOM_FRACTION = 0.2;
40
+
41
+
42
+ /**
43
+ * Bytes kept free below the memory limit.
44
+ *
45
+ * A guard that only reacts once RSS equals the limit has already lost: the
46
+ * allocation that breaches it happens between two samples, and the spike that
47
+ * matters — an upstream response landing all at once — arrives in far less
48
+ * than one sample interval.
49
+ */
50
+ export function deriveMemoryHeadroomBytes(memoryLimitBytes: number): number {
51
+ return Math.ceil(memoryLimitBytes * MEMORY_HEADROOM_FRACTION);
52
+ }
53
+
54
+
55
+ /**
56
+ * Slack added on top of every key's concurrency limit.
57
+ *
58
+ * Operator configuration, not something a key carries about itself: it is
59
+ * deliberate hidden allowance so a run that lands a few requests over the
60
+ * number an operator chose is not refused at that number. The effective
61
+ * ceiling is `limit + tolerance`, and only requests past it are refused.
62
+ */
63
+ export function resolveConcurrencyTolerance(): number {
64
+ const value = readOptionalInt(
65
+ "CARTETHYIA_CONCURRENCY_TOLERANCE",
66
+ CONFIG_SPEC.CARTETHYIA_CONCURRENCY_TOLERANCE,
67
+ );
68
+ return value === undefined ? 0 : Math.max(0, value);
69
+ }
70
+
@@ -1,73 +1,37 @@
1
- /**
2
- * Security resolvers: the application encryption key, the abuse-protection
3
- * ceiling, and model-abuse strike/ban windows.
4
- */
5
-
6
- import { CONFIG_SPEC, readInt, readRequired } from "./spec";
7
-
8
- const ENCRYPTION_KEY_LENGTH = 32;
9
-
10
- /**
11
- * Per-IP request ceiling for the unauthenticated abuse-protection layer.
12
- * Defaults to 240 requests per window so an unset env does not mean unlimited;
13
- * operators tune it via `IP_RATE_MAX_PER_WINDOW`.
14
- */
15
- export function resolveIpRateLimit(): number {
16
- return readInt("IP_RATE_MAX_PER_WINDOW", CONFIG_SPEC.IP_RATE_MAX_PER_WINDOW);
17
- }
18
-
19
- /** Invalid-model attempts inside the window that ban a client address. */
20
- export function resolveModelStrikeThreshold(): number {
21
- return readInt(
22
- "CARTETHYIA_MODEL_STRIKE_THRESHOLD",
23
- CONFIG_SPEC.CARTETHYIA_MODEL_STRIKE_THRESHOLD,
24
- );
25
- }
26
-
27
- /** Sliding window (ms) that keeps an invalid-model attempt countable. */
28
- export function resolveModelStrikeWindowMs(): number {
29
- return readInt(
30
- "CARTETHYIA_MODEL_STRIKE_WINDOW_MS",
31
- CONFIG_SPEC.CARTETHYIA_MODEL_STRIKE_WINDOW_MS,
32
- );
33
- }
34
-
35
- /** How long (ms) a model-abuse ban lasts before it lapses on its own. */
36
- export function resolveModelBanTtlMs(): number {
37
- return readInt("CARTETHYIA_MODEL_BAN_TTL_MS", CONFIG_SPEC.CARTETHYIA_MODEL_BAN_TTL_MS);
38
- }
39
-
40
- /**
41
- * The longest model-abuse ban the console will record, however long a request
42
- * asks for.
43
- *
44
- * A separate knob from the ban TTL because the two are different decisions:
45
- * the TTL is what a graduated ban gets, this is the ceiling on what an
46
- * operator may ask for by hand. Raising one was never meant to raise the other.
47
- */
48
- export function resolveModelBanMaxTtlMs(): number {
49
- return readInt(
50
- "CARTETHYIA_MODEL_BAN_MAX_TTL_MS",
51
- CONFIG_SPEC.CARTETHYIA_MODEL_BAN_MAX_TTL_MS,
52
- );
53
- }
54
-
55
- /** Reads the application encryption key without caching it. */
56
- export function requireEncryptionKeyEnv(): string {
57
- return readRequired("CARTETHYIA_ENCRYPTION_KEY", CONFIG_SPEC.CARTETHYIA_ENCRYPTION_KEY);
58
- }
59
-
60
- /** Decodes and validates a base64 or hexadecimal 256-bit encryption key. */
61
- export function decodeEncryptionKey(raw: string): Buffer {
62
- const candidate = raw.trim();
63
- const buf = /^[0-9a-fA-F]{64}$/.test(candidate)
64
- ? Buffer.from(candidate, "hex")
65
- : Buffer.from(candidate, "base64");
66
- if (buf.length !== ENCRYPTION_KEY_LENGTH) {
67
- throw new Error(
68
- `CARTETHYIA_ENCRYPTION_KEY must decode to exactly ${ENCRYPTION_KEY_LENGTH} bytes (got ${buf.length}); ` +
69
- "provide a base64 or hex encoded 256-bit key",
70
- );
71
- }
72
- return buf;
73
- }
1
+ /**
2
+ * Security resolvers: the application encryption key and the abuse-protection
3
+ * ceiling.
4
+ */
5
+
6
+ import { CONFIG_SPEC, readInt, readRequired } from "./spec";
7
+
8
+ const ENCRYPTION_KEY_LENGTH = 32;
9
+
10
+ /**
11
+ * Per-IP request ceiling for the unauthenticated abuse-protection layer.
12
+ * Defaults to 240 requests per window so an unset env does not mean unlimited;
13
+ * operators tune it via `IP_RATE_MAX_PER_WINDOW`.
14
+ */
15
+ export function resolveIpRateLimit(): number {
16
+ return readInt("IP_RATE_MAX_PER_WINDOW", CONFIG_SPEC.IP_RATE_MAX_PER_WINDOW);
17
+ }
18
+
19
+ /** Reads the application encryption key without caching it. */
20
+ export function requireEncryptionKeyEnv(): string {
21
+ return readRequired("CARTETHYIA_ENCRYPTION_KEY", CONFIG_SPEC.CARTETHYIA_ENCRYPTION_KEY);
22
+ }
23
+
24
+ /** Decodes and validates a base64 or hexadecimal 256-bit encryption key. */
25
+ export function decodeEncryptionKey(raw: string): Buffer {
26
+ const candidate = raw.trim();
27
+ const buf = /^[0-9a-fA-F]{64}$/.test(candidate)
28
+ ? Buffer.from(candidate, "hex")
29
+ : Buffer.from(candidate, "base64");
30
+ if (buf.length !== ENCRYPTION_KEY_LENGTH) {
31
+ throw new Error(
32
+ `CARTETHYIA_ENCRYPTION_KEY must decode to exactly ${ENCRYPTION_KEY_LENGTH} bytes (got ${buf.length}); ` +
33
+ "provide a base64 or hex encoded 256-bit key",
34
+ );
35
+ }
36
+ return buf;
37
+ }