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.
- package/.env.example +106 -113
- package/dist/dashboard/assets/{BannedPage-Dskhyp0l.js → BannedPage-P76QLGJA.js} +1 -1
- package/dist/dashboard/assets/BannedPage-P76QLGJA.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolDetailPage--SJhk7qQ.js → CliToolDetailPage-DdBggJS9.js} +1 -1
- package/dist/dashboard/assets/CliToolDetailPage-DdBggJS9.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolsPage-C203VT-c.js → CliToolsPage-aN1iXhhP.js} +1 -1
- package/dist/dashboard/assets/CliToolsPage-aN1iXhhP.js.gz +0 -0
- package/dist/dashboard/assets/{CombosPage-BcS7kGG1.js → CombosPage-DE-F1nxZ.js} +1 -1
- package/dist/dashboard/assets/CombosPage-DE-F1nxZ.js.gz +0 -0
- package/dist/dashboard/assets/{ConsoleLogPage-ChmiSLwO.js → ConsoleLogPage-5SpmfLyw.js} +1 -1
- package/dist/dashboard/assets/ConsoleLogPage-5SpmfLyw.js.gz +0 -0
- package/dist/dashboard/assets/{CustomizationPage-uujbBYwC.js → CustomizationPage-3CUxMy2X.js} +1 -1
- package/dist/dashboard/assets/CustomizationPage-3CUxMy2X.js.gz +0 -0
- package/dist/dashboard/assets/{HealthEventsModal-fKAWd-OQ.js → HealthEventsModal-BkmJQKC0.js} +1 -1
- package/dist/dashboard/assets/HealthEventsModal-BkmJQKC0.js.gz +0 -0
- package/dist/dashboard/assets/{LoginPage-DoOUthbG.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-CFu1qGcT.js → ProviderDetailPage-CEJw6Guo.js} +3 -3
- package/dist/dashboard/assets/ProviderDetailPage-CEJw6Guo.js.gz +0 -0
- package/dist/dashboard/assets/{ProvidersPage-CnlJZbxF.js → ProvidersPage-CEplAR-i.js} +1 -1
- package/dist/dashboard/assets/ProvidersPage-CEplAR-i.js.gz +0 -0
- package/dist/dashboard/assets/{ProxyPage-B9mBTweh.js → ProxyPage-CAN7hy0f.js} +1 -1
- package/dist/dashboard/assets/ProxyPage-CAN7hy0f.js.gz +0 -0
- package/dist/dashboard/assets/{QuotaPage-BueSWX5D.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-DQ9oDRpi.js → SetupPage-_bCaTM0v.js} +1 -1
- package/dist/dashboard/assets/SetupPage-_bCaTM0v.js.gz +0 -0
- package/dist/dashboard/assets/{SortableList-DrKeMcGN.js → SortableList-fI7JkdIB.js} +1 -1
- package/dist/dashboard/assets/SortableList-fI7JkdIB.js.gz +0 -0
- package/dist/dashboard/assets/{StudioPage-qjuYtccc.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-DOddwoc0.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-BeLRhsmt.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-BfZ3SCTM.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/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/dist/dashboard/assets/BannedPage-Dskhyp0l.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage--SJhk7qQ.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-C203VT-c.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-BcS7kGG1.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-ChmiSLwO.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-uujbBYwC.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-fKAWd-OQ.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-DoOUthbG.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-CA1kLs45.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-C5Z-UBz1.js +0 -1
- package/dist/dashboard/assets/OverviewPage-C5Z-UBz1.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-CFu1qGcT.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-CnlJZbxF.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-B9mBTweh.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-BueSWX5D.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-B0x00MVY.js +0 -1
- package/dist/dashboard/assets/SettingsPage-B0x00MVY.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-DQ9oDRpi.js.gz +0 -0
- package/dist/dashboard/assets/SortableList-DrKeMcGN.js.gz +0 -0
- package/dist/dashboard/assets/StudioPage-qjuYtccc.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-Be7aNviQ.js +0 -56
- package/dist/dashboard/assets/UsagePage-Be7aNviQ.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-DOddwoc0.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-BeLRhsmt.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-BfZ3SCTM.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
package/src/config/spec.ts
CHANGED
|
@@ -1,378 +1,358 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Spec-driven configuration reader shared by the `src/config/` domain modules.
|
|
3
|
-
*
|
|
4
|
-
* `CONFIG_SPEC` is the canonical declaration of every environment variable
|
|
5
|
-
* `src/config/` reads: name, kind, default, and bounds in one place. The
|
|
6
|
-
* resolvers in the sibling domain modules are thin readers over it, so a new
|
|
7
|
-
* knob cannot be added without declaring a row here plus a `.env.example`
|
|
8
|
-
* line. Bounds are enforced on read so a malformed operator value throws at
|
|
9
|
-
* the boundary instead of silently coercing.
|
|
10
|
-
*
|
|
11
|
-
* Module-specific environment reads that only affect a single subsystem (e.g.
|
|
12
|
-
* logger level, telemetry payload storage, Postgres pool tuning) still live
|
|
13
|
-
* next to their consumer. No I/O, no caching — readers are pure with respect
|
|
14
|
-
* to `process.env` at call time.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
/** Default directory for the built dashboard static assets. */
|
|
18
|
-
const DEFAULT_DASHBOARD_DIST = "./dist/dashboard";
|
|
19
|
-
/** Default upstream client version host for Kimi's OAuth origin. */
|
|
20
|
-
const DEFAULT_KIMI_OAUTH_HOST = "https://auth.kimi.com";
|
|
21
|
-
/** Base for Kimi Code's quota/billing surface; the usage path joins on this. */
|
|
22
|
-
const DEFAULT_KIMI_QUOTA_BASE_URL = "https://api.kimi.com/coding/v1";
|
|
23
|
-
|
|
24
|
-
// ─── Config spec ────────────────────────────────────────────────────────────
|
|
25
|
-
|
|
26
|
-
interface IntConfigEntry {
|
|
27
|
-
readonly kind: "int";
|
|
28
|
-
readonly default: number;
|
|
29
|
-
readonly min: number;
|
|
30
|
-
readonly max: number;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
interface TextConfigEntry {
|
|
34
|
-
readonly kind: "text";
|
|
35
|
-
readonly default: string;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** Absent means "no value"; an empty/whitespace value also reads as absent. */
|
|
39
|
-
interface OptionalTextConfigEntry {
|
|
40
|
-
readonly kind: "optional-text";
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Absent or empty means "unset" — distinct from an explicit value, because
|
|
45
|
-
* consumers apply their own fallback (cgroup detection, egress default) rather
|
|
46
|
-
* than a fixed default here.
|
|
47
|
-
*/
|
|
48
|
-
interface OptionalIntConfigEntry {
|
|
49
|
-
readonly kind: "optional-int";
|
|
50
|
-
readonly min: number;
|
|
51
|
-
readonly max: number;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
interface RequiredTextConfigEntry {
|
|
55
|
-
readonly kind: "required";
|
|
56
|
-
readonly error: string;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/** Enabled unless the literal string `"false"` is set. */
|
|
60
|
-
interface FlagConfigEntry {
|
|
61
|
-
readonly kind: "flag";
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/** Comma-separated list; an empty result reads as absent. */
|
|
65
|
-
interface ListConfigEntry {
|
|
66
|
-
readonly kind: "list";
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export type ConfigEntry =
|
|
70
|
-
| IntConfigEntry
|
|
71
|
-
| TextConfigEntry
|
|
72
|
-
| OptionalTextConfigEntry
|
|
73
|
-
| OptionalIntConfigEntry
|
|
74
|
-
| RequiredTextConfigEntry
|
|
75
|
-
| FlagConfigEntry
|
|
76
|
-
| ListConfigEntry;
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* The single declaration of every environment variable `src/config/` reads.
|
|
80
|
-
*
|
|
81
|
-
* Adding a knob means adding a row here plus a `.env.example` line. Bounds are
|
|
82
|
-
* enforced on read so a malformed operator value throws at the boundary
|
|
83
|
-
* instead of silently coercing.
|
|
84
|
-
*/
|
|
85
|
-
export const CONFIG_SPEC = {
|
|
86
|
-
// HTTP listener + dashboard runtime
|
|
87
|
-
PORT: { kind: "int", default: 12_800, min: 1, max: 65_535 },
|
|
88
|
-
DASHBOARD_DIST: { kind: "text", default: DEFAULT_DASHBOARD_DIST },
|
|
89
|
-
CARTETHYIA_API_KEY: { kind: "optional-text" },
|
|
90
|
-
CARTETHYIA_SERVER_MAX_BODY_BYTES: {
|
|
91
|
-
kind: "int",
|
|
92
|
-
// A 500K-token context is roughly 2 MB of text, so 4 MB admits every real
|
|
93
|
-
// request while halving the worst-case hold.
|
|
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
|
-
//
|
|
194
|
-
//
|
|
195
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
*
|
|
289
|
-
*
|
|
290
|
-
*
|
|
291
|
-
*
|
|
292
|
-
*
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
const raw = process.env[name];
|
|
360
|
-
if (!raw) throw new Error(entry.error);
|
|
361
|
-
return raw;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
/** Reads a flag: enabled unless the literal `"false"` is set. */
|
|
365
|
-
export function readFlag(name: string, entry: FlagConfigEntry): boolean {
|
|
366
|
-
void entry;
|
|
367
|
-
return process.env[name] !== "false";
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
/** Reads a comma-separated list, dropping empty entries. */
|
|
371
|
-
export function readList(name: string, entry: ListConfigEntry): readonly string[] | undefined {
|
|
372
|
-
void entry;
|
|
373
|
-
const entries = process.env[name]
|
|
374
|
-
?.split(",")
|
|
375
|
-
.map((value) => value.trim())
|
|
376
|
-
.filter(Boolean);
|
|
377
|
-
return entries && entries.length > 0 ? entries : undefined;
|
|
378
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Spec-driven configuration reader shared by the `src/config/` domain modules.
|
|
3
|
+
*
|
|
4
|
+
* `CONFIG_SPEC` is the canonical declaration of every environment variable
|
|
5
|
+
* `src/config/` reads: name, kind, default, and bounds in one place. The
|
|
6
|
+
* resolvers in the sibling domain modules are thin readers over it, so a new
|
|
7
|
+
* knob cannot be added without declaring a row here plus a `.env.example`
|
|
8
|
+
* line. Bounds are enforced on read so a malformed operator value throws at
|
|
9
|
+
* the boundary instead of silently coercing.
|
|
10
|
+
*
|
|
11
|
+
* Module-specific environment reads that only affect a single subsystem (e.g.
|
|
12
|
+
* logger level, telemetry payload storage, Postgres pool tuning) still live
|
|
13
|
+
* next to their consumer. No I/O, no caching — readers are pure with respect
|
|
14
|
+
* to `process.env` at call time.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** Default directory for the built dashboard static assets. */
|
|
18
|
+
const DEFAULT_DASHBOARD_DIST = "./dist/dashboard";
|
|
19
|
+
/** Default upstream client version host for Kimi's OAuth origin. */
|
|
20
|
+
const DEFAULT_KIMI_OAUTH_HOST = "https://auth.kimi.com";
|
|
21
|
+
/** Base for Kimi Code's quota/billing surface; the usage path joins on this. */
|
|
22
|
+
const DEFAULT_KIMI_QUOTA_BASE_URL = "https://api.kimi.com/coding/v1";
|
|
23
|
+
|
|
24
|
+
// ─── Config spec ────────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
interface IntConfigEntry {
|
|
27
|
+
readonly kind: "int";
|
|
28
|
+
readonly default: number;
|
|
29
|
+
readonly min: number;
|
|
30
|
+
readonly max: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface TextConfigEntry {
|
|
34
|
+
readonly kind: "text";
|
|
35
|
+
readonly default: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Absent means "no value"; an empty/whitespace value also reads as absent. */
|
|
39
|
+
interface OptionalTextConfigEntry {
|
|
40
|
+
readonly kind: "optional-text";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Absent or empty means "unset" — distinct from an explicit value, because
|
|
45
|
+
* consumers apply their own fallback (cgroup detection, egress default) rather
|
|
46
|
+
* than a fixed default here.
|
|
47
|
+
*/
|
|
48
|
+
interface OptionalIntConfigEntry {
|
|
49
|
+
readonly kind: "optional-int";
|
|
50
|
+
readonly min: number;
|
|
51
|
+
readonly max: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface RequiredTextConfigEntry {
|
|
55
|
+
readonly kind: "required";
|
|
56
|
+
readonly error: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Enabled unless the literal string `"false"` is set. */
|
|
60
|
+
interface FlagConfigEntry {
|
|
61
|
+
readonly kind: "flag";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Comma-separated list; an empty result reads as absent. */
|
|
65
|
+
interface ListConfigEntry {
|
|
66
|
+
readonly kind: "list";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export type ConfigEntry =
|
|
70
|
+
| IntConfigEntry
|
|
71
|
+
| TextConfigEntry
|
|
72
|
+
| OptionalTextConfigEntry
|
|
73
|
+
| OptionalIntConfigEntry
|
|
74
|
+
| RequiredTextConfigEntry
|
|
75
|
+
| FlagConfigEntry
|
|
76
|
+
| ListConfigEntry;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The single declaration of every environment variable `src/config/` reads.
|
|
80
|
+
*
|
|
81
|
+
* Adding a knob means adding a row here plus a `.env.example` line. Bounds are
|
|
82
|
+
* enforced on read so a malformed operator value throws at the boundary
|
|
83
|
+
* instead of silently coercing.
|
|
84
|
+
*/
|
|
85
|
+
export const CONFIG_SPEC = {
|
|
86
|
+
// HTTP listener + dashboard runtime
|
|
87
|
+
PORT: { kind: "int", default: 12_800, min: 1, max: 65_535 },
|
|
88
|
+
DASHBOARD_DIST: { kind: "text", default: DEFAULT_DASHBOARD_DIST },
|
|
89
|
+
CARTETHYIA_API_KEY: { kind: "optional-text" },
|
|
90
|
+
CARTETHYIA_SERVER_MAX_BODY_BYTES: {
|
|
91
|
+
kind: "int",
|
|
92
|
+
// A 500K-token context is roughly 2 MB of text, so 4 MB admits every real
|
|
93
|
+
// request while halving the worst-case hold. How many may be in flight is
|
|
94
|
+
// admission's job, so an operator with larger payloads raises this and
|
|
95
|
+
// sets the ceiling in Settings.
|
|
96
|
+
default: 4 * 1024 * 1024,
|
|
97
|
+
min: 1024,
|
|
98
|
+
max: Number.MAX_SAFE_INTEGER,
|
|
99
|
+
},
|
|
100
|
+
CARTETHYIA_SERVER_IDLE_TIMEOUT: { kind: "int", default: 60, min: 0, max: 86_400 },
|
|
101
|
+
|
|
102
|
+
// Graceful-shutdown drain: how long in-flight requests may finish naturally
|
|
103
|
+
// before the drain aborts the stragglers. Sized above the common case (a
|
|
104
|
+
// typical response) but below the stream stall bound so a wedged stream is
|
|
105
|
+
// still cut within the process budget.
|
|
106
|
+
CARTETHYIA_SHUTDOWN_DRAIN_WINDOW_MS: {
|
|
107
|
+
kind: "int",
|
|
108
|
+
default: 20_000,
|
|
109
|
+
min: 0,
|
|
110
|
+
max: 600_000,
|
|
111
|
+
},
|
|
112
|
+
// Optional ceiling on in-flight requests per process. Left unset, the limit
|
|
113
|
+
// is derived from the memory limit and the body ceiling (see
|
|
114
|
+
// config/resources.ts): 2 GiB/4 MiB → 64, 512 MiB/4 MiB → 16, 256 MiB/16 MiB
|
|
115
|
+
// → 2. Set it to admit more than memory alone would allow — which is the
|
|
116
|
+
// operator's call, and is reported rather than corrected — or less, to keep
|
|
117
|
+
// a slow upstream from being handed more concurrency than it can serve.
|
|
118
|
+
// Optional-int, not int-with-a-zero-default: "unset" and "explicitly zero"
|
|
119
|
+
// must not be the same thing. Zero is not a valid ceiling, so a default of 0
|
|
120
|
+
// would need a second sentinel to mean "derive".
|
|
121
|
+
// Slack above each key's concurrency limit. Effective ceiling is
|
|
122
|
+
// limit + tolerance; only requests past it are refused.
|
|
123
|
+
CARTETHYIA_CONCURRENCY_TOLERANCE: { kind: "optional-int", min: 0, max: 1024 },
|
|
124
|
+
// How many gateway processes serve this deployment, and which one this is.
|
|
125
|
+
// Count > 1 requires Linux, CARTETHYIA_DB_MODE=full, and a REACHABLE Redis —
|
|
126
|
+
// checked at boot, because a half-configured multi-process deploy starts
|
|
127
|
+
// fine and then enforces every per-key limit N times over. Lite stays
|
|
128
|
+
// single-process: two processes on one SQLite file corrupt it.
|
|
129
|
+
CARTETHYIA_PROCESS_COUNT: { kind: "int", default: 1, min: 1, max: 16 },
|
|
130
|
+
// Which process this is (0 – count-1). Mandatory when count > 1: a process
|
|
131
|
+
// that does not know its index cannot own a shard of anything.
|
|
132
|
+
CARTETHYIA_PROCESS_INDEX: { kind: "int", default: 0, min: 0, max: 15 },
|
|
133
|
+
// Optional secret for the operator drain endpoint (`POST /admin/drain`). When
|
|
134
|
+
// set, a process can be stopped gracefully on platforms where a catchable
|
|
135
|
+
// signal cannot be delivered to it (notably Windows, where `process.kill` of
|
|
136
|
+
// a console-less process runs no JS handler). The endpoint additionally
|
|
137
|
+
// requires a loopback peer and a timing-safe token match; unset = the route
|
|
138
|
+
// does not exist.
|
|
139
|
+
CARTETHYIA_DRAIN_TOKEN: { kind: "optional-text" },
|
|
140
|
+
|
|
141
|
+
// Network policy (trusted proxy + SSRF)
|
|
142
|
+
TRUSTED_PROXY_CIDRS: { kind: "list" },
|
|
143
|
+
CARTETHYIA_ALLOW_PRIVATE_UPSTREAMS: { kind: "flag" },
|
|
144
|
+
CARTETHYIA_ALLOWED_NETWORKS: { kind: "list" },
|
|
145
|
+
CARTETHYIA_MAX_REDIRECTS: { kind: "optional-int", min: 0, max: 10 },
|
|
146
|
+
|
|
147
|
+
// Runtime memory
|
|
148
|
+
CARTETHYIA_MEMORY_LIMIT_BYTES: {
|
|
149
|
+
kind: "optional-int",
|
|
150
|
+
min: 1,
|
|
151
|
+
max: Number.MAX_SAFE_INTEGER,
|
|
152
|
+
},
|
|
153
|
+
// Opt-in forced garbage collection under memory pressure. Off by default:
|
|
154
|
+
// see `observability/runtime-metrics.ts` — a forced collection is not
|
|
155
|
+
// reclamation, and a collection that runs while requests are in flight adds
|
|
156
|
+
// a latency spike. This gates the pressure path only; idle reclaim runs
|
|
157
|
+
// unconditionally, because reclaiming when nothing is in flight stalls
|
|
158
|
+
// nothing.
|
|
159
|
+
CARTETHYIA_FORCED_GC: { kind: "flag" },
|
|
160
|
+
// How often runtime gauges (memory + bounded-collection sizes) are sampled.
|
|
161
|
+
CARTETHYIA_RUNTIME_METRICS_INTERVAL_MS: {
|
|
162
|
+
kind: "int",
|
|
163
|
+
default: 30_000,
|
|
164
|
+
min: 1_000,
|
|
165
|
+
max: 300_000,
|
|
166
|
+
},
|
|
167
|
+
/**
|
|
168
|
+
* Periodic maintenance tasks switched off entirely, by name. A small
|
|
169
|
+
* deployment that does not need live quota or version polling can cut the
|
|
170
|
+
* heaviest sweeps here instead of carrying one flag per task.
|
|
171
|
+
*/
|
|
172
|
+
CARTETHYIA_DISABLED_TASKS: { kind: "list" },
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* How long a day's telemetry is kept, in days.
|
|
176
|
+
*
|
|
177
|
+
* Whole raw event files past this age are removed. No summary or compacted
|
|
178
|
+
* form replaces the individual requests.
|
|
179
|
+
*
|
|
180
|
+
* This is the knob that decides how long the request list, per-request
|
|
181
|
+
* filters, and the detail drawer stay complete.
|
|
182
|
+
*/
|
|
183
|
+
CARTETHYIA_TELEMETRY_RETENTION_DAYS: { kind: "int", default: 30, min: 3, max: 365 },
|
|
184
|
+
CARTETHYIA_TELEMETRY_PAYLOAD_MAX_BYTES: {
|
|
185
|
+
kind: "int",
|
|
186
|
+
default: 16 * 1024 * 1024,
|
|
187
|
+
min: 1 * 1024 * 1024,
|
|
188
|
+
max: 16 * 1024 * 1024,
|
|
189
|
+
},
|
|
190
|
+
|
|
191
|
+
// Upstream request timeout + retry backoff
|
|
192
|
+
//
|
|
193
|
+
// Strictly under 90s: a client whose own deadline is 90s must never be
|
|
194
|
+
// waiting on a gateway that is still holding a request open past it, and a
|
|
195
|
+
// provider that has not answered within 75s is not answering — the request
|
|
196
|
+
// is queued upstream or the egress is stuck, and the honest move is to fail
|
|
197
|
+
// over to another account rather than burn the caller's whole budget on one.
|
|
198
|
+
// 120s outlived the clients that call this, which cap lower.
|
|
199
|
+
CARTETHYIA_UPSTREAM_TIMEOUT_MS: { kind: "int", default: 75_000, min: 5_000, max: 600_000 },
|
|
200
|
+
CARTETHYIA_STREAM_STALL_TIMEOUT_MS: {
|
|
201
|
+
kind: "int",
|
|
202
|
+
default: 360_000,
|
|
203
|
+
min: 60_000,
|
|
204
|
+
max: 600_000,
|
|
205
|
+
},
|
|
206
|
+
CARTETHYIA_STREAM_FIRST_CHUNK_TIMEOUT_MS: {
|
|
207
|
+
kind: "int",
|
|
208
|
+
default: 200_000,
|
|
209
|
+
min: 30_000,
|
|
210
|
+
max: 300_000,
|
|
211
|
+
},
|
|
212
|
+
CARTETHYIA_FALLBACK_RETRY_BASE_MS: { kind: "int", default: 100, min: 0, max: 60_000 },
|
|
213
|
+
CARTETHYIA_FALLBACK_RETRY_CAP_MS: { kind: "int", default: 2_000, min: 0, max: 300_000 },
|
|
214
|
+
CARTETHYIA_ROUTE_MAX_ATTEMPTS: { kind: "int", default: 8, min: 1, max: 64 },
|
|
215
|
+
|
|
216
|
+
// Account/pool health cooldown delays. Each is the fallback used when the
|
|
217
|
+
// upstream error states no explicit reset window; an upstream
|
|
218
|
+
// `Retry-After`/reset header or message always wins over these.
|
|
219
|
+
CARTETHYIA_ACCOUNT_RATE_LIMIT_COOLDOWN_MS: { kind: "int", default: 900_000, min: 0, max: 86_400_000 },
|
|
220
|
+
CARTETHYIA_ACCOUNT_QUOTA_COOLDOWN_MS: { kind: "int", default: 3_600_000, min: 0, max: 86_400_000 },
|
|
221
|
+
CARTETHYIA_ACCOUNT_MODEL_CAPACITY_COOLDOWN_MS: { kind: "int", default: 120_000, min: 0, max: 86_400_000 },
|
|
222
|
+
CARTETHYIA_ACCOUNT_TRANSIENT_COOLDOWN_MS: { kind: "int", default: 30_000, min: 0, max: 86_400_000 },
|
|
223
|
+
/**
|
|
224
|
+
* An unmapped upstream failure cools the account down for 15 minutes.
|
|
225
|
+
*
|
|
226
|
+
* A shorter window puts the account straight back into rotation and re-dials
|
|
227
|
+
* an upstream that has just answered something this gateway does not
|
|
228
|
+
* understand — so the same unknown error is retried every minute, and an
|
|
229
|
+
* account that is actually broken contributes a steady stream of failed
|
|
230
|
+
* attempts instead of one. Fifteen minutes is long enough for a transient
|
|
231
|
+
* provider condition to clear on its own and short enough that no operator
|
|
232
|
+
* action is needed to recover it. It is still a cooldown with a deadline,
|
|
233
|
+
* never `disabled`, so nothing here can strand an account.
|
|
234
|
+
*/
|
|
235
|
+
CARTETHYIA_ACCOUNT_UNCLASSIFIED_COOLDOWN_MS: {
|
|
236
|
+
kind: "int",
|
|
237
|
+
default: 900_000,
|
|
238
|
+
min: 0,
|
|
239
|
+
max: 86_400_000,
|
|
240
|
+
},
|
|
241
|
+
CARTETHYIA_ACCOUNT_COOLDOWN_ESCALATION_MAX_MS: { kind: "int", default: 3_600_000, min: 0, max: 86_400_000 },
|
|
242
|
+
CARTETHYIA_POOL_COOLDOWN_MS: { kind: "int", default: 120_000, min: 0, max: 86_400_000 },
|
|
243
|
+
CARTETHYIA_ACCOUNT_AUTH_REJECTED_COOLDOWN_MS: { kind: "int", default: 300_000, min: 0, max: 86_400_000 },
|
|
244
|
+
CARTETHYIA_ACCOUNT_POLICY_BLOCK_COOLDOWN_MS: { kind: "int", default: 300_000, min: 0, max: 86_400_000 },
|
|
245
|
+
CARTETHYIA_ACCOUNT_DISABLED_RECOVERY_MS: { kind: "int", default: 86_400_000, min: 0, max: 604_800_000 },
|
|
246
|
+
|
|
247
|
+
// Proxy pool agents
|
|
248
|
+
CARTETHYIA_PROXY_MAX_SOCKETS: { kind: "int", default: 100, min: 1, max: 100_000 },
|
|
249
|
+
CARTETHYIA_PROXY_MAX_FREE_SOCKETS: { kind: "int", default: 20, min: 0, max: 100_000 },
|
|
250
|
+
|
|
251
|
+
// Security (encryption key, public origin, abuse ceiling)
|
|
252
|
+
IP_RATE_MAX_PER_WINDOW: { kind: "int", default: 240, min: 1, max: Number.MAX_SAFE_INTEGER },
|
|
253
|
+
CARTETHYIA_ENCRYPTION_KEY: {
|
|
254
|
+
kind: "required",
|
|
255
|
+
error:
|
|
256
|
+
"CARTETHYIA_ENCRYPTION_KEY is required to encrypt credentials and hash API keys",
|
|
257
|
+
},
|
|
258
|
+
CARTETHYIA_PUBLIC_ORIGIN: {
|
|
259
|
+
kind: "required",
|
|
260
|
+
error:
|
|
261
|
+
"CARTETHYIA_PUBLIC_ORIGIN is required to build OAuth redirect URIs (e.g. https://cartethyia.example.com)",
|
|
262
|
+
},
|
|
263
|
+
|
|
264
|
+
// Provider OAuth hosts
|
|
265
|
+
CARTETHYIA_KIMI_OAUTH_HOST: { kind: "text", default: DEFAULT_KIMI_OAUTH_HOST },
|
|
266
|
+
/**
|
|
267
|
+
* Loopback port the browser-OAuth callback listener binds.
|
|
268
|
+
*
|
|
269
|
+
* Configurable because the default is a fixed number: a busy port would strand
|
|
270
|
+
* the operator, since the login could not start and there would be no way to
|
|
271
|
+
* move it. Codex's authorization server still expects 59653, so changing this
|
|
272
|
+
* only helps the providers whose redirect URI is not allowlisted upstream.
|
|
273
|
+
*/
|
|
274
|
+
CARTETHYIA_OAUTH_CALLBACK_PORT: { kind: "int", default: 59_653, min: 1_024, max: 65_535 },
|
|
275
|
+
/**
|
|
276
|
+
* Kimi Code quota/billing base. The `/usages` path joins on this, so the
|
|
277
|
+
* default includes the `/v1` segment the bundled provider metadata implies.
|
|
278
|
+
*/
|
|
279
|
+
CARTETHYIA_KIMI_QUOTA_BASE_URL: { kind: "text", default: DEFAULT_KIMI_QUOTA_BASE_URL },
|
|
280
|
+
/**
|
|
281
|
+
* GitHub Enterprise domain for the Copilot device login, e.g.
|
|
282
|
+
* `company.ghe.com`. Empty (the default) means github.com. GitHub Enterprise
|
|
283
|
+
* serves the device flow and the token-mint endpoint from its own domain, so
|
|
284
|
+
* an operator on one has no other way to point the login at it.
|
|
285
|
+
*/
|
|
286
|
+
CARTETHYIA_GITHUB_ENTERPRISE_DOMAIN: { kind: "text", default: "" },
|
|
287
|
+
/**
|
|
288
|
+
* Free-tier OpenCode ids the listing still advertises but upstream refuses.
|
|
289
|
+
* Replaces the bundled list outright; the single token `none` trusts the
|
|
290
|
+
* listing completely.
|
|
291
|
+
*
|
|
292
|
+
* The bundled ids were measured, not declared — upstream can start serving
|
|
293
|
+
* a refused id or start refusing a served one at any time, and either way
|
|
294
|
+
* the correct answer stopped being the one compiled in. This is the escape
|
|
295
|
+
* hatch that needs no deploy.
|
|
296
|
+
*/
|
|
297
|
+
CARTETHYIA_OPENCODE_UNAVAILABLE_FREE: { kind: "list" },
|
|
298
|
+
} as const satisfies Readonly<Record<string, ConfigEntry>>;
|
|
299
|
+
|
|
300
|
+
// ─── Spec readers ───────────────────────────────────────────────────────────
|
|
301
|
+
|
|
302
|
+
/** Reads a bounded integer, throwing on a malformed or out-of-range value. */
|
|
303
|
+
export function readInt(name: string, entry: IntConfigEntry): number {
|
|
304
|
+
const raw = process.env[name];
|
|
305
|
+
if (raw === undefined || raw.trim() === "") return entry.default;
|
|
306
|
+
const value = Number(raw);
|
|
307
|
+
if (!Number.isInteger(value) || value < entry.min || value > entry.max) {
|
|
308
|
+
throw new Error(`${name} must be an integer between ${entry.min} and ${entry.max}`);
|
|
309
|
+
}
|
|
310
|
+
return value;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** Reads a string with a fallback, treating an empty value as absent. */
|
|
314
|
+
export function readText(name: string, entry: TextConfigEntry): string {
|
|
315
|
+
const raw = process.env[name]?.trim();
|
|
316
|
+
return raw && raw.length > 0 ? raw : entry.default;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** Reads an optional string; empty or whitespace reads as absent. */
|
|
320
|
+
export function readOptionalText(name: string, entry: OptionalTextConfigEntry): string | undefined {
|
|
321
|
+
void entry;
|
|
322
|
+
const raw = process.env[name]?.trim();
|
|
323
|
+
return raw && raw.length > 0 ? raw : undefined;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** Reads an optional bounded integer; absent or empty reads as `undefined`. */
|
|
327
|
+
export function readOptionalInt(name: string, entry: OptionalIntConfigEntry): number | undefined {
|
|
328
|
+
const raw = process.env[name];
|
|
329
|
+
if (raw === undefined || raw.trim() === "") return undefined;
|
|
330
|
+
const value = Number(raw);
|
|
331
|
+
if (!Number.isInteger(value) || value < entry.min || value > entry.max) {
|
|
332
|
+
throw new Error(`${name} must be an integer between ${entry.min} and ${entry.max}`);
|
|
333
|
+
}
|
|
334
|
+
return value;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/** Reads a required string, throwing with the entry's documented message. */
|
|
338
|
+
export function readRequired(name: string, entry: RequiredTextConfigEntry): string {
|
|
339
|
+
const raw = process.env[name];
|
|
340
|
+
if (!raw) throw new Error(entry.error);
|
|
341
|
+
return raw;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/** Reads a flag: enabled unless the literal `"false"` is set. */
|
|
345
|
+
export function readFlag(name: string, entry: FlagConfigEntry): boolean {
|
|
346
|
+
void entry;
|
|
347
|
+
return process.env[name] !== "false";
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** Reads a comma-separated list, dropping empty entries. */
|
|
351
|
+
export function readList(name: string, entry: ListConfigEntry): readonly string[] | undefined {
|
|
352
|
+
void entry;
|
|
353
|
+
const entries = process.env[name]
|
|
354
|
+
?.split(",")
|
|
355
|
+
.map((value) => value.trim())
|
|
356
|
+
.filter(Boolean);
|
|
357
|
+
return entries && entries.length > 0 ? entries : undefined;
|
|
358
|
+
}
|