cartethyia 2.6.6 → 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.
- package/.env.example +106 -113
- package/dist/dashboard/assets/{BannedPage-1v5rzKMl.js → BannedPage-P76QLGJA.js} +1 -1
- package/dist/dashboard/assets/BannedPage-P76QLGJA.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolDetailPage-chMg74ct.js → CliToolDetailPage-DdBggJS9.js} +1 -1
- package/dist/dashboard/assets/CliToolDetailPage-DdBggJS9.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolsPage-CKG-Vxrt.js → CliToolsPage-aN1iXhhP.js} +1 -1
- package/dist/dashboard/assets/CliToolsPage-aN1iXhhP.js.gz +0 -0
- package/dist/dashboard/assets/{CombosPage-JYmVKzIx.js → CombosPage-DE-F1nxZ.js} +1 -1
- package/dist/dashboard/assets/CombosPage-DE-F1nxZ.js.gz +0 -0
- package/dist/dashboard/assets/{ConsoleLogPage-CPflWDry.js → ConsoleLogPage-5SpmfLyw.js} +1 -1
- package/dist/dashboard/assets/ConsoleLogPage-5SpmfLyw.js.gz +0 -0
- package/dist/dashboard/assets/{CustomizationPage-B0EYUE47.js → CustomizationPage-3CUxMy2X.js} +1 -1
- package/dist/dashboard/assets/CustomizationPage-3CUxMy2X.js.gz +0 -0
- package/dist/dashboard/assets/{HealthEventsModal-F1c4lkNG.js → HealthEventsModal-BkmJQKC0.js} +1 -1
- package/dist/dashboard/assets/HealthEventsModal-BkmJQKC0.js.gz +0 -0
- package/dist/dashboard/assets/{LoginPage-BYTpzcoU.js → LoginPage-B_0FUKWc.js} +1 -1
- package/dist/dashboard/assets/LoginPage-B_0FUKWc.js.gz +0 -0
- package/dist/dashboard/assets/{ModelPicker-CA1kLs45.js → ModelPicker-DKVslqG3.js} +1 -1
- package/dist/dashboard/assets/ModelPicker-DKVslqG3.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-D-H4MEiT.js +1 -0
- package/dist/dashboard/assets/OverviewPage-D-H4MEiT.js.gz +0 -0
- package/dist/dashboard/assets/{ProviderDetailPage-CM4MNy45.js → ProviderDetailPage-CEJw6Guo.js} +3 -3
- package/dist/dashboard/assets/ProviderDetailPage-CEJw6Guo.js.gz +0 -0
- package/dist/dashboard/assets/{ProvidersPage-Bsoqg2HG.js → ProvidersPage-CEplAR-i.js} +1 -1
- package/dist/dashboard/assets/ProvidersPage-CEplAR-i.js.gz +0 -0
- package/dist/dashboard/assets/{ProxyPage-8G3doV7j.js → ProxyPage-CAN7hy0f.js} +1 -1
- package/dist/dashboard/assets/ProxyPage-CAN7hy0f.js.gz +0 -0
- package/dist/dashboard/assets/{QuotaPage-CVIQq6JG.js → QuotaPage-BjldQwIU.js} +1 -1
- package/dist/dashboard/assets/QuotaPage-BjldQwIU.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-DUIoLRxk.js +1 -0
- package/dist/dashboard/assets/SettingsPage-DUIoLRxk.js.gz +0 -0
- package/dist/dashboard/assets/{SetupPage-BCx07-bj.js → SetupPage-_bCaTM0v.js} +1 -1
- package/dist/dashboard/assets/SetupPage-_bCaTM0v.js.gz +0 -0
- package/dist/dashboard/assets/{SortableList-MhkB6msu.js → SortableList-fI7JkdIB.js} +1 -1
- package/dist/dashboard/assets/SortableList-fI7JkdIB.js.gz +0 -0
- package/dist/dashboard/assets/{StudioPage-CTqK6Nsj.js → StudioPage-CHVZqskZ.js} +1 -1
- package/dist/dashboard/assets/StudioPage-CHVZqskZ.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-DiFBymHj.js +56 -0
- package/dist/dashboard/assets/UsagePage-DiFBymHj.js.gz +0 -0
- package/dist/dashboard/assets/{api-keys-C23S9sgp.js → api-keys-DFY_fZU4.js} +1 -1
- package/dist/dashboard/assets/api-keys-DFY_fZU4.js.gz +0 -0
- package/dist/dashboard/assets/app-BOJ1_xUB.css +2 -0
- package/dist/dashboard/assets/app-BOJ1_xUB.css.gz +0 -0
- package/dist/dashboard/assets/{assert-DU6qrMA_.js → assert-CwdnZARt.js} +1 -1
- package/dist/dashboard/assets/assert-CwdnZARt.js.gz +0 -0
- package/dist/dashboard/assets/{badge-Bkz_-RrM.js → badge-Dr-uf0Au.js} +1 -1
- package/dist/dashboard/assets/badge-Dr-uf0Au.js.gz +0 -0
- package/dist/dashboard/assets/{banner-C-7y3VKV.js → banner-BddmMdoS.js} +1 -1
- package/dist/dashboard/assets/banner-BddmMdoS.js.gz +0 -0
- package/dist/dashboard/assets/{card-BOPBuBzD.js → card-CK0PXWlX.js} +1 -1
- package/dist/dashboard/assets/card-CK0PXWlX.js.gz +0 -0
- package/dist/dashboard/assets/{cli-tools-BaFUuFFF.js → cli-tools-CcB6mGh2.js} +1 -1
- package/dist/dashboard/assets/cli-tools-CcB6mGh2.js.gz +0 -0
- package/dist/dashboard/assets/{clipboard-button-e-Mlk0FH.js → clipboard-button-DGb2P9Yx.js} +1 -1
- package/dist/dashboard/assets/clipboard-button-DGb2P9Yx.js.gz +0 -0
- package/dist/dashboard/assets/{dialog-Iec_ywbD.js → dialog-LkfuL50u.js} +1 -1
- package/dist/dashboard/assets/dialog-LkfuL50u.js.gz +0 -0
- package/dist/dashboard/assets/{drawer-Bs8MIIHc.js → drawer-ZbEZ_84c.js} +1 -1
- package/dist/dashboard/assets/drawer-ZbEZ_84c.js.gz +0 -0
- package/dist/dashboard/assets/{entry-DcobaKFu.js → entry-BJfsVMvA.js} +1 -1
- package/dist/dashboard/assets/entry-BJfsVMvA.js.gz +0 -0
- package/dist/dashboard/assets/{entry-CBHzQ6Qh.js → entry-CWBY5A0f.js} +3 -3
- package/dist/dashboard/assets/entry-CWBY5A0f.js.gz +0 -0
- package/dist/dashboard/assets/{entry-BRszm4_Z.js → entry-CvC1-Zqx.js} +1 -1
- package/dist/dashboard/assets/entry-CvC1-Zqx.js.gz +0 -0
- package/dist/dashboard/assets/{github-badge-BuDsrsPn.js → github-badge-BxpznFLt.js} +1 -1
- package/dist/dashboard/assets/github-badge-BxpznFLt.js.gz +0 -0
- package/dist/dashboard/assets/{index-Ck4uwuYb.js → index-CDzP3ovJ.js} +2 -2
- package/dist/dashboard/assets/index-CDzP3ovJ.js.gz +0 -0
- package/dist/dashboard/assets/{inline-DToMICwl.js → inline-Ae13MQ5w.js} +1 -1
- package/dist/dashboard/assets/inline-Ae13MQ5w.js.gz +0 -0
- package/dist/dashboard/assets/{input-WlyHWeG2.js → input-D23fo7cp.js} +1 -1
- package/dist/dashboard/assets/input-D23fo7cp.js.gz +0 -0
- package/dist/dashboard/assets/{layout-DaJ5baJr.js → layout-CSsxjj5P.js} +1 -1
- package/dist/dashboard/assets/layout-CSsxjj5P.js.gz +0 -0
- package/dist/dashboard/assets/{list-row-B3oSK13z.js → list-row-dmbwZMDY.js} +1 -1
- package/dist/dashboard/assets/list-row-dmbwZMDY.js.gz +0 -0
- package/dist/dashboard/assets/{live-ta6GMb-8.js → live-BG1oUsZZ.js} +1 -1
- package/dist/dashboard/assets/live-BG1oUsZZ.js.gz +0 -0
- package/dist/dashboard/assets/minimize-2-D-MN8W8a.js +1 -0
- package/dist/dashboard/assets/minimize-2-D-MN8W8a.js.gz +0 -0
- package/dist/dashboard/assets/{mutation-g_fduiNw.js → mutation-DSF-e4ho.js} +1 -1
- package/dist/dashboard/assets/mutation-DSF-e4ho.js.gz +0 -0
- package/dist/dashboard/assets/{numbers-Dulqgys5.js → numbers-Bm_aECY7.js} +1 -1
- package/dist/dashboard/assets/{numbers-Dulqgys5.js.gz → numbers-Bm_aECY7.js.gz} +0 -0
- package/dist/dashboard/assets/{query-keys-UETqfytp.js → query-keys-CF56nYIV.js} +1 -1
- package/dist/dashboard/assets/query-keys-CF56nYIV.js.gz +0 -0
- package/dist/dashboard/assets/{routing-BPUo-W8d.js → routing-CeA71n7G.js} +1 -1
- package/dist/dashboard/assets/routing-CeA71n7G.js.gz +0 -0
- package/dist/dashboard/assets/{select-BrrUDUPc.js → select-KIT0rC6G.js} +1 -1
- package/dist/dashboard/assets/select-KIT0rC6G.js.gz +0 -0
- package/dist/dashboard/assets/settings-B_2gkePf.js +1 -0
- package/dist/dashboard/assets/settings-B_2gkePf.js.gz +0 -0
- package/dist/dashboard/assets/{stack-B1w5BM0u.js → stack-D7WOSzcX.js} +1 -1
- package/dist/dashboard/assets/stack-D7WOSzcX.js.gz +0 -0
- package/dist/dashboard/assets/{state-DhZBfWgx.js → state-BBxtKZOQ.js} +1 -1
- package/dist/dashboard/assets/state-BBxtKZOQ.js.gz +0 -0
- package/dist/dashboard/assets/{switch-yD7D99Lg.js → switch-DNS_D5Ea.js} +1 -1
- package/dist/dashboard/assets/switch-DNS_D5Ea.js.gz +0 -0
- package/dist/dashboard/assets/{useMutation-EDFRkn1i.js → useMutation-BgoxdoMk.js} +1 -1
- package/dist/dashboard/assets/useMutation-BgoxdoMk.js.gz +0 -0
- package/dist/dashboard/index.html +1 -1
- package/dist/dashboard/index.html.gz +0 -0
- package/dist/dashboard/precompressed-assets.json +51 -51
- package/package.json +96 -96
- package/src/boot/app.ts +714 -722
- package/src/boot/dependencies.ts +729 -750
- package/src/config/resources.ts +70 -137
- package/src/config/security.ts +37 -73
- package/src/config/spec.ts +358 -378
- package/src/console/console-router.ts +225 -233
- package/src/console/domain-registration.ts +381 -391
- package/src/console/remote-support/contracts.ts +3 -1
- package/src/console/remote-support/service.ts +115 -6
- package/src/main.ts +167 -169
- package/src/observability/metrics.ts +467 -457
- package/src/observability/runtime-metrics.ts +269 -254
- package/src/protocol/gateway-error.ts +232 -233
- package/src/providers/integrations/commandcode/commandcode.ts +134 -104
- package/src/providers/integrations/commandcode/discovery.ts +6 -5
- package/src/providers/integrations/qoder/qoder.ts +597 -23
- package/src/providers/operations/account-health-service.ts +52 -3
- package/src/security/abuse.ts +17 -0
- package/src/transport/guards/auth-guard.ts +109 -127
- package/src/transport/guards/pipeline.ts +220 -226
- package/src/transport/guards/request-context.ts +238 -318
- package/src/transport/request/inflight.ts +21 -2
- package/src/workers/scheduler.ts +73 -6
- package/dist/dashboard/assets/BannedPage-1v5rzKMl.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-chMg74ct.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-CKG-Vxrt.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-JYmVKzIx.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-CPflWDry.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-B0EYUE47.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-F1c4lkNG.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-BYTpzcoU.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-CA1kLs45.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-DIYZRAcb.js +0 -1
- package/dist/dashboard/assets/OverviewPage-DIYZRAcb.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-CM4MNy45.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-Bsoqg2HG.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-8G3doV7j.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-CVIQq6JG.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-BgXvRN2Q.js +0 -1
- package/dist/dashboard/assets/SettingsPage-BgXvRN2Q.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-BCx07-bj.js.gz +0 -0
- package/dist/dashboard/assets/SortableList-MhkB6msu.js.gz +0 -0
- package/dist/dashboard/assets/StudioPage-CTqK6Nsj.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-Do65OWbr.js +0 -56
- package/dist/dashboard/assets/UsagePage-Do65OWbr.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-C23S9sgp.js.gz +0 -0
- package/dist/dashboard/assets/app-BUSiYL6F.css +0 -2
- package/dist/dashboard/assets/app-BUSiYL6F.css.gz +0 -0
- package/dist/dashboard/assets/assert-DU6qrMA_.js.gz +0 -0
- package/dist/dashboard/assets/badge-Bkz_-RrM.js.gz +0 -0
- package/dist/dashboard/assets/banner-C-7y3VKV.js.gz +0 -0
- package/dist/dashboard/assets/card-BOPBuBzD.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-BaFUuFFF.js.gz +0 -0
- package/dist/dashboard/assets/clipboard-button-e-Mlk0FH.js.gz +0 -0
- package/dist/dashboard/assets/dialog-Iec_ywbD.js.gz +0 -0
- package/dist/dashboard/assets/drawer-Bs8MIIHc.js.gz +0 -0
- package/dist/dashboard/assets/entry-BRszm4_Z.js.gz +0 -0
- package/dist/dashboard/assets/entry-CBHzQ6Qh.js.gz +0 -0
- package/dist/dashboard/assets/entry-DcobaKFu.js.gz +0 -0
- package/dist/dashboard/assets/github-badge-BuDsrsPn.js.gz +0 -0
- package/dist/dashboard/assets/index-Ck4uwuYb.js.gz +0 -0
- package/dist/dashboard/assets/inline-DToMICwl.js.gz +0 -0
- package/dist/dashboard/assets/input-WlyHWeG2.js.gz +0 -0
- package/dist/dashboard/assets/layout-DaJ5baJr.js.gz +0 -0
- package/dist/dashboard/assets/list-row-B3oSK13z.js.gz +0 -0
- package/dist/dashboard/assets/live-ta6GMb-8.js.gz +0 -0
- package/dist/dashboard/assets/model-bans-BmYTmbAk.js +0 -1
- package/dist/dashboard/assets/model-bans-BmYTmbAk.js.gz +0 -0
- package/dist/dashboard/assets/mutation-g_fduiNw.js.gz +0 -0
- package/dist/dashboard/assets/query-keys-UETqfytp.js.gz +0 -0
- package/dist/dashboard/assets/routing-BPUo-W8d.js.gz +0 -0
- package/dist/dashboard/assets/select-BrrUDUPc.js.gz +0 -0
- package/dist/dashboard/assets/settings--3hxSyry.js +0 -1
- package/dist/dashboard/assets/settings--3hxSyry.js.gz +0 -0
- package/dist/dashboard/assets/stack-B1w5BM0u.js.gz +0 -0
- package/dist/dashboard/assets/state-DhZBfWgx.js.gz +0 -0
- package/dist/dashboard/assets/switch-yD7D99Lg.js.gz +0 -0
- package/dist/dashboard/assets/useMutation-EDFRkn1i.js.gz +0 -0
- package/src/console/admin/model-abuse/contracts.ts +0 -38
- package/src/console/admin/model-abuse/routes.ts +0 -97
- package/src/security/model-abuse.ts +0 -704
- package/src/transport/guards/concurrency-guard.ts +0 -125
- /package/dist/dashboard/assets/{app-MdVfwraB.js → app-CaXZ9SCn.js} +0 -0
- /package/dist/dashboard/assets/{app-MdVfwraB.js.gz → app-CaXZ9SCn.js.gz} +0 -0
|
@@ -1,254 +1,269 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Periodic runtime gauges: process memory plus bounded-collection sizes.
|
|
3
|
-
*
|
|
4
|
-
* Sampling is driven by `ScheduledTaskRegistry` (unref'd interval) rather than
|
|
5
|
-
* a dedicated timer so it shares the process's existing maintenance lifecycle
|
|
6
|
-
* and stops with it. Map/collection sizes are read through closures supplied at
|
|
7
|
-
* composition time; this module owns no references to the routing or pool
|
|
8
|
-
* layers, keeping the observability boundary one-directional.
|
|
9
|
-
*/
|
|
10
|
-
import { readFileSync } from "node:fs";
|
|
11
|
-
import
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
readonly
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
*
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
*
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
*
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
*
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
metrics.
|
|
194
|
-
metrics.
|
|
195
|
-
metrics.
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
this.
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
*
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Periodic runtime gauges: process memory plus bounded-collection sizes.
|
|
3
|
+
*
|
|
4
|
+
* Sampling is driven by `ScheduledTaskRegistry` (unref'd interval) rather than
|
|
5
|
+
* a dedicated timer so it shares the process's existing maintenance lifecycle
|
|
6
|
+
* and stops with it. Map/collection sizes are read through closures supplied at
|
|
7
|
+
* composition time; this module owns no references to the routing or pool
|
|
8
|
+
* layers, keeping the observability boundary one-directional.
|
|
9
|
+
*/
|
|
10
|
+
import { readFileSync } from "node:fs";
|
|
11
|
+
import os from "node:os";
|
|
12
|
+
import { metrics } from "./metrics";
|
|
13
|
+
import { trackMemoryUsage } from "./performance-metrics";
|
|
14
|
+
import { log } from "./logger";
|
|
15
|
+
import { resolveMemoryLimitOverrideBytes } from "../config/timeouts";
|
|
16
|
+
import { CONFIG_SPEC, readFlag } from "../config/spec";
|
|
17
|
+
import type { TelemetryBacklogSnapshot } from "./telemetry-buffer";
|
|
18
|
+
/**
|
|
19
|
+
* The memory figures a shed decision needs.
|
|
20
|
+
*
|
|
21
|
+
* Published once per sample instead of re-measured per request:
|
|
22
|
+
* `process.memoryUsage()` is not free, and the headroom guard runs on every
|
|
23
|
+
* request. A snapshot up to one sample interval old is the right trade — the
|
|
24
|
+
* guard is protecting against a spike that arrives over seconds, not one
|
|
25
|
+
* request's worth of allocation.
|
|
26
|
+
*/
|
|
27
|
+
export interface RuntimeMemorySnapshot {
|
|
28
|
+
readonly rssBytes: number;
|
|
29
|
+
/** The resolved limit; `undefined` when none could be discovered. */
|
|
30
|
+
readonly limitBytes: number | undefined;
|
|
31
|
+
readonly sampledAtMs: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Live collection sizes sampled alongside process memory. */
|
|
35
|
+
export interface RuntimeMetricSources {
|
|
36
|
+
readonly routingRoundRobinEntries: () => { readonly combo: number; readonly provider: number };
|
|
37
|
+
/**
|
|
38
|
+
* Tracked keys in the in-memory IP abuse store, or 0 when the store is
|
|
39
|
+
* Redis-backed. Unlike the routing maps this one is fed by client-supplied
|
|
40
|
+
* identities, so its size is the leak signal to watch.
|
|
41
|
+
*/
|
|
42
|
+
readonly ipAbuseKeys: () => number;
|
|
43
|
+
/**
|
|
44
|
+
* The telemetry backlog's current shape, or `undefined` when no buffer is
|
|
45
|
+
* wired (tests, and the console-only shell).
|
|
46
|
+
*/
|
|
47
|
+
readonly telemetryBacklog?: () => TelemetryBacklogSnapshot | undefined;
|
|
48
|
+
/** Tracked keys in the in-process quota cache tier (0 when Redis-only paths are idle). */
|
|
49
|
+
readonly quotaCacheEntries: () => number;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Entries in the per-provider model-catalog cache. Bounded, but a count that
|
|
53
|
+
* sits at the cap means every rebuild is evicting something it needs.
|
|
54
|
+
*/
|
|
55
|
+
readonly modelCatalogEntries: () => number;
|
|
56
|
+
/**
|
|
57
|
+
* Live in-flight request count. Zero means the gateway is idle, so a
|
|
58
|
+
* synchronous full collection is safe (no request to stall).
|
|
59
|
+
*/
|
|
60
|
+
readonly inFlight: () => number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Cooldown between memory-pressure warnings so a sustained high-water mark does not flood logs. */
|
|
64
|
+
const PRESSURE_LOG_WINDOW_MS = 60_000;
|
|
65
|
+
/** Cooldown between proactive garbage collections while pressure persists. */
|
|
66
|
+
const PRESSURE_GC_WINDOW_MS = 60_000;
|
|
67
|
+
/** Fraction of the resolved limit at which memory pressure is reported. */
|
|
68
|
+
const PRESSURE_THRESHOLD = 0.8;
|
|
69
|
+
/**
|
|
70
|
+
* Fallback limit when neither an override nor a cgroup limit is discoverable
|
|
71
|
+
* (native development, macOS, Windows).
|
|
72
|
+
*
|
|
73
|
+
* A fixed 512 MiB made every machine without a cgroup look like a 512 MiB
|
|
74
|
+
* container. On a desktop running the gateway natively that is far below what
|
|
75
|
+
* the machine has, so the pressure check fired against a number that described
|
|
76
|
+
* nothing — "RSS 430 MiB exceeds 80% of the 512 MiB limit" on a machine with
|
|
77
|
+
* gigabytes free — and the concurrency cap derived from it admitted a handful
|
|
78
|
+
* of requests while every tenant-level setting sat unreachable above it.
|
|
79
|
+
*
|
|
80
|
+
* The host's real memory is the honest stand-in. It is what the process can
|
|
81
|
+
* actually grow into here, and it is what "no container limit" means.
|
|
82
|
+
*/
|
|
83
|
+
function defaultMemoryLimitBytes(): number {
|
|
84
|
+
try {
|
|
85
|
+
const total = os.totalmem();
|
|
86
|
+
if (Number.isFinite(total) && total > 0) return total;
|
|
87
|
+
} catch {
|
|
88
|
+
// Platform without totalmem; fall through to the last-resort constant.
|
|
89
|
+
}
|
|
90
|
+
return LAST_RESORT_MEMORY_LIMIT_BYTES;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Used only when the host's memory cannot be read at all. Deliberately large
|
|
95
|
+
* so an unmeasurable host is not treated as a small container.
|
|
96
|
+
*/
|
|
97
|
+
const LAST_RESORT_MEMORY_LIMIT_BYTES = 4 * 1024 * 1024 * 1024;
|
|
98
|
+
/** Cooldown between idle full collections. */
|
|
99
|
+
const IDLE_GC_WINDOW_MS = 5 * 60_000;
|
|
100
|
+
/** RSS retained above the post-collection baseline before another idle collection runs. */
|
|
101
|
+
const IDLE_GC_MIN_GROWTH_BYTES = 32 * 1024 * 1024;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Resolves the effective memory limit for the process.
|
|
105
|
+
*
|
|
106
|
+
* Precedence: an explicit `CARTETHYIA_MEMORY_LIMIT_BYTES` override, then the
|
|
107
|
+
* cgroup v2/v1 limit when running in a container. Falls back to
|
|
108
|
+
* {@link DEFAULT_MEMORY_LIMIT_BYTES} when no limit is discoverable (native
|
|
109
|
+
* development, macOS, Windows) so pressure-based reclaim still runs.
|
|
110
|
+
*/
|
|
111
|
+
export function resolveMemoryLimitBytes(): number | undefined {
|
|
112
|
+
const override = resolveMemoryLimitOverrideBytes();
|
|
113
|
+
if (override !== undefined) return override;
|
|
114
|
+
if (process.platform !== "linux") return defaultMemoryLimitBytes();
|
|
115
|
+
for (const path of [
|
|
116
|
+
"/sys/fs/cgroup/memory.max",
|
|
117
|
+
"/sys/fs/cgroup/memory/memory.limit_in_bytes",
|
|
118
|
+
]) {
|
|
119
|
+
try {
|
|
120
|
+
const content = readFileSync(path, "utf8").trim();
|
|
121
|
+
if (content === "max") continue;
|
|
122
|
+
const value = Number(content);
|
|
123
|
+
// The v1 unlimited sentinel is a huge finite number; treat it as unknown.
|
|
124
|
+
if (Number.isInteger(value) && value > 0 && value < 2 ** 53) return value;
|
|
125
|
+
} catch {
|
|
126
|
+
// File absent (cgroup v1 vs v2) or unreadable — try the next source.
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return defaultMemoryLimitBytes();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Samples process memory and bounded-collection sizes into the metric registry. */
|
|
133
|
+
export class RuntimeMetricsSampler {
|
|
134
|
+
private readonly limitBytes: number | undefined;
|
|
135
|
+
private readonly gcOnPressure: boolean;
|
|
136
|
+
private lastPressureLogAt = 0;
|
|
137
|
+
private lastGcAt = 0;
|
|
138
|
+
private lastIdleGcAt = 0;
|
|
139
|
+
private rssAtLastGc = 0;
|
|
140
|
+
private inFlightSource: () => number = () => 0;
|
|
141
|
+
/**
|
|
142
|
+
* The latest snapshot, published once per sample. Stays undefined until the
|
|
143
|
+
* first sample runs; consumers must treat that as "unknown", not as zero.
|
|
144
|
+
*/
|
|
145
|
+
private snapshot: RuntimeMemorySnapshot | undefined;
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Binds the live in-flight count after the request state store exists. The
|
|
149
|
+
* sampler is constructed before that store, so the source is late-bound.
|
|
150
|
+
*/
|
|
151
|
+
bindInFlight(source: () => number): void {
|
|
152
|
+
this.inFlightSource = source;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* The last sampled memory state, for consumers on a hot path.
|
|
157
|
+
*
|
|
158
|
+
* Undefined before the first sample: a guard that read zero as "plenty of
|
|
159
|
+
* room" would disable itself exactly when it matters, so callers must fall
|
|
160
|
+
* back to their own judgement when this is absent.
|
|
161
|
+
*/
|
|
162
|
+
memorySnapshot(): RuntimeMemorySnapshot | undefined {
|
|
163
|
+
return this.snapshot;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Publishes the effective concurrency ceiling.
|
|
168
|
+
*
|
|
169
|
+
* Late-bound like `bindInFlight`: the ceiling is derived from the memory
|
|
170
|
+
* limit, which is known here, but the guard that owns it is constructed in
|
|
171
|
+
* the transport layer.
|
|
172
|
+
*/
|
|
173
|
+
|
|
174
|
+
constructor(
|
|
175
|
+
private readonly sources: RuntimeMetricSources,
|
|
176
|
+
limitBytes: number | undefined = resolveMemoryLimitBytes(),
|
|
177
|
+
// Off by default: a forced collection is not how memory is meant to come
|
|
178
|
+
// back. The synchronous form (`Bun.gc(true)`) stops the shared event loop
|
|
179
|
+
// outright, so every concurrent request stalls inside it — which reads as
|
|
180
|
+
// a latency spike with no upstream cause. And it hides the actual defect:
|
|
181
|
+
// a process that only returns memory when something forces it to is a
|
|
182
|
+
// process that is still holding it. Bounded caches, released streams, and
|
|
183
|
+
// batched writers reclaim naturally; this is opt-in for an operator who
|
|
184
|
+
// has measured that they need it.
|
|
185
|
+
gcOnPressure = readFlag("CARTETHYIA_FORCED_GC", CONFIG_SPEC.CARTETHYIA_FORCED_GC),
|
|
186
|
+
) {
|
|
187
|
+
this.limitBytes = limitBytes;
|
|
188
|
+
this.gcOnPressure = gcOnPressure;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
sample(): void {
|
|
192
|
+
const memory = process.memoryUsage();
|
|
193
|
+
metrics.cartethyia_memory_rss_bytes.set(memory.rss);
|
|
194
|
+
metrics.cartethyia_memory_heap_used_bytes.set(memory.heapUsed);
|
|
195
|
+
metrics.cartethyia_memory_heap_total_bytes.set(memory.heapTotal);
|
|
196
|
+
metrics.cartethyia_memory_limit_bytes.set(this.limitBytes ?? 0);
|
|
197
|
+
this.snapshot = {
|
|
198
|
+
rssBytes: memory.rss,
|
|
199
|
+
limitBytes: this.limitBytes,
|
|
200
|
+
sampledAtMs: Date.now(),
|
|
201
|
+
};
|
|
202
|
+
trackMemoryUsage("rss", memory.rss);
|
|
203
|
+
trackMemoryUsage("heap_used", memory.heapUsed);
|
|
204
|
+
trackMemoryUsage("heap_total", memory.heapTotal);
|
|
205
|
+
trackMemoryUsage("external", memory.external);
|
|
206
|
+
|
|
207
|
+
const roundRobin = this.sources.routingRoundRobinEntries();
|
|
208
|
+
metrics.cartethyia_routing_roundrobin_entries.set(roundRobin.combo, { scope: "combo" });
|
|
209
|
+
metrics.cartethyia_routing_roundrobin_entries.set(roundRobin.provider, { scope: "provider" });
|
|
210
|
+
metrics.cartethyia_ip_abuse_keys.set(this.sources.ipAbuseKeys());
|
|
211
|
+
metrics.cartethyia_quota_cache_entries.set(this.sources.quotaCacheEntries());
|
|
212
|
+
metrics.cartethyia_model_catalog_entries.set(this.sources.modelCatalogEntries());
|
|
213
|
+
metrics.cartethyia_process_in_flight.set(this.inFlightSource());
|
|
214
|
+
const backlog = this.sources.telemetryBacklog?.();
|
|
215
|
+
if (backlog !== undefined) {
|
|
216
|
+
metrics.cartethyia_telemetry_backlog_items.set(backlog.items);
|
|
217
|
+
metrics.cartethyia_telemetry_backlog_bytes.set(backlog.bytes);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
this.checkPressure(memory.rss);
|
|
221
|
+
this.checkIdleReclaim(memory.rss);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
private checkPressure(rss: number): void {
|
|
225
|
+
if (this.limitBytes === undefined || rss <= this.limitBytes * PRESSURE_THRESHOLD) return;
|
|
226
|
+
const now = Date.now();
|
|
227
|
+
if (this.gcOnPressure && now - this.lastGcAt >= PRESSURE_GC_WINDOW_MS) {
|
|
228
|
+
this.lastGcAt = now;
|
|
229
|
+
// Asynchronous GC: reclaims before the limit is reached without stalling
|
|
230
|
+
// in-flight requests on the shared event loop. A synchronous full
|
|
231
|
+
// collection here would add a latency spike to every concurrent request.
|
|
232
|
+
Bun.gc(false);
|
|
233
|
+
}
|
|
234
|
+
if (now - this.lastPressureLogAt < PRESSURE_LOG_WINDOW_MS) return;
|
|
235
|
+
this.lastPressureLogAt = now;
|
|
236
|
+
log.warn(
|
|
237
|
+
`[memory] RSS ${(rss / 1024 / 1024).toFixed(1)}MiB exceeds 80% of the ` +
|
|
238
|
+
`${(this.limitBytes / 1024 / 1024).toFixed(1)}MiB limit`,
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Reclaims memory while the gateway is idle.
|
|
244
|
+
*
|
|
245
|
+
* Triggered on RSS growth rather than heap growth: this process holds most of
|
|
246
|
+
* its footprint off-heap (adapters, TLS contexts, buffers), and `heapUsed`
|
|
247
|
+
* barely moves when those are allocated — a heap-based gate never opens, which
|
|
248
|
+
* is why an idle gateway held its startup peak indefinitely.
|
|
249
|
+
*
|
|
250
|
+
* A synchronous full collection is safe here because no request is in flight to
|
|
251
|
+
* stall, and it is the only form that actually returns pages to the OS; the
|
|
252
|
+
* async form used under pressure does not.
|
|
253
|
+
*
|
|
254
|
+
* Not gated on `gcOnPressure` the way `checkPressure` is. The opt-in exists
|
|
255
|
+
* because a synchronous collection stalls concurrent requests, and this path
|
|
256
|
+
* only runs when there is nothing in flight to stall — so the reason the flag
|
|
257
|
+
* is off does not apply to it. Gating it meant a gateway with no traffic at
|
|
258
|
+
* all never reclaimed anything and held its startup peak for hours.
|
|
259
|
+
*/
|
|
260
|
+
private checkIdleReclaim(rss: number): void {
|
|
261
|
+
if (this.inFlightSource() > 0) return;
|
|
262
|
+
const now = Date.now();
|
|
263
|
+
if (now - this.lastIdleGcAt < IDLE_GC_WINDOW_MS) return;
|
|
264
|
+
if (this.rssAtLastGc !== 0 && rss - this.rssAtLastGc < IDLE_GC_MIN_GROWTH_BYTES) return;
|
|
265
|
+
this.lastIdleGcAt = now;
|
|
266
|
+
Bun.gc(true);
|
|
267
|
+
this.rssAtLastGc = process.memoryUsage().rss;
|
|
268
|
+
}
|
|
269
|
+
}
|