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.
Files changed (189) hide show
  1. package/.env.example +106 -113
  2. package/dist/dashboard/assets/{BannedPage-1v5rzKMl.js → BannedPage-P76QLGJA.js} +1 -1
  3. package/dist/dashboard/assets/BannedPage-P76QLGJA.js.gz +0 -0
  4. package/dist/dashboard/assets/{CliToolDetailPage-chMg74ct.js → CliToolDetailPage-DdBggJS9.js} +1 -1
  5. package/dist/dashboard/assets/CliToolDetailPage-DdBggJS9.js.gz +0 -0
  6. package/dist/dashboard/assets/{CliToolsPage-CKG-Vxrt.js → CliToolsPage-aN1iXhhP.js} +1 -1
  7. package/dist/dashboard/assets/CliToolsPage-aN1iXhhP.js.gz +0 -0
  8. package/dist/dashboard/assets/{CombosPage-JYmVKzIx.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-CPflWDry.js → ConsoleLogPage-5SpmfLyw.js} +1 -1
  11. package/dist/dashboard/assets/ConsoleLogPage-5SpmfLyw.js.gz +0 -0
  12. package/dist/dashboard/assets/{CustomizationPage-B0EYUE47.js → CustomizationPage-3CUxMy2X.js} +1 -1
  13. package/dist/dashboard/assets/CustomizationPage-3CUxMy2X.js.gz +0 -0
  14. package/dist/dashboard/assets/{HealthEventsModal-F1c4lkNG.js → HealthEventsModal-BkmJQKC0.js} +1 -1
  15. package/dist/dashboard/assets/HealthEventsModal-BkmJQKC0.js.gz +0 -0
  16. package/dist/dashboard/assets/{LoginPage-BYTpzcoU.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-CM4MNy45.js → ProviderDetailPage-CEJw6Guo.js} +3 -3
  23. package/dist/dashboard/assets/ProviderDetailPage-CEJw6Guo.js.gz +0 -0
  24. package/dist/dashboard/assets/{ProvidersPage-Bsoqg2HG.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-8G3doV7j.js → ProxyPage-CAN7hy0f.js} +1 -1
  27. package/dist/dashboard/assets/ProxyPage-CAN7hy0f.js.gz +0 -0
  28. package/dist/dashboard/assets/{QuotaPage-CVIQq6JG.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-BCx07-bj.js → SetupPage-_bCaTM0v.js} +1 -1
  33. package/dist/dashboard/assets/SetupPage-_bCaTM0v.js.gz +0 -0
  34. package/dist/dashboard/assets/{SortableList-MhkB6msu.js → SortableList-fI7JkdIB.js} +1 -1
  35. package/dist/dashboard/assets/SortableList-fI7JkdIB.js.gz +0 -0
  36. package/dist/dashboard/assets/{StudioPage-CTqK6Nsj.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-CBHzQ6Qh.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-Ck4uwuYb.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-BrrUDUPc.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/providers/operations/account-health-service.ts +52 -3
  123. package/src/security/abuse.ts +17 -0
  124. package/src/transport/guards/auth-guard.ts +109 -127
  125. package/src/transport/guards/pipeline.ts +220 -226
  126. package/src/transport/guards/request-context.ts +238 -318
  127. package/src/transport/request/inflight.ts +21 -2
  128. package/src/workers/scheduler.ts +73 -6
  129. package/dist/dashboard/assets/BannedPage-1v5rzKMl.js.gz +0 -0
  130. package/dist/dashboard/assets/CliToolDetailPage-chMg74ct.js.gz +0 -0
  131. package/dist/dashboard/assets/CliToolsPage-CKG-Vxrt.js.gz +0 -0
  132. package/dist/dashboard/assets/CombosPage-JYmVKzIx.js.gz +0 -0
  133. package/dist/dashboard/assets/ConsoleLogPage-CPflWDry.js.gz +0 -0
  134. package/dist/dashboard/assets/CustomizationPage-B0EYUE47.js.gz +0 -0
  135. package/dist/dashboard/assets/HealthEventsModal-F1c4lkNG.js.gz +0 -0
  136. package/dist/dashboard/assets/LoginPage-BYTpzcoU.js.gz +0 -0
  137. package/dist/dashboard/assets/ModelPicker-CA1kLs45.js.gz +0 -0
  138. package/dist/dashboard/assets/OverviewPage-DIYZRAcb.js +0 -1
  139. package/dist/dashboard/assets/OverviewPage-DIYZRAcb.js.gz +0 -0
  140. package/dist/dashboard/assets/ProviderDetailPage-CM4MNy45.js.gz +0 -0
  141. package/dist/dashboard/assets/ProvidersPage-Bsoqg2HG.js.gz +0 -0
  142. package/dist/dashboard/assets/ProxyPage-8G3doV7j.js.gz +0 -0
  143. package/dist/dashboard/assets/QuotaPage-CVIQq6JG.js.gz +0 -0
  144. package/dist/dashboard/assets/SettingsPage-BgXvRN2Q.js +0 -1
  145. package/dist/dashboard/assets/SettingsPage-BgXvRN2Q.js.gz +0 -0
  146. package/dist/dashboard/assets/SetupPage-BCx07-bj.js.gz +0 -0
  147. package/dist/dashboard/assets/SortableList-MhkB6msu.js.gz +0 -0
  148. package/dist/dashboard/assets/StudioPage-CTqK6Nsj.js.gz +0 -0
  149. package/dist/dashboard/assets/UsagePage-Do65OWbr.js +0 -56
  150. package/dist/dashboard/assets/UsagePage-Do65OWbr.js.gz +0 -0
  151. package/dist/dashboard/assets/api-keys-C23S9sgp.js.gz +0 -0
  152. package/dist/dashboard/assets/app-BUSiYL6F.css +0 -2
  153. package/dist/dashboard/assets/app-BUSiYL6F.css.gz +0 -0
  154. package/dist/dashboard/assets/assert-DU6qrMA_.js.gz +0 -0
  155. package/dist/dashboard/assets/badge-Bkz_-RrM.js.gz +0 -0
  156. package/dist/dashboard/assets/banner-C-7y3VKV.js.gz +0 -0
  157. package/dist/dashboard/assets/card-BOPBuBzD.js.gz +0 -0
  158. package/dist/dashboard/assets/cli-tools-BaFUuFFF.js.gz +0 -0
  159. package/dist/dashboard/assets/clipboard-button-e-Mlk0FH.js.gz +0 -0
  160. package/dist/dashboard/assets/dialog-Iec_ywbD.js.gz +0 -0
  161. package/dist/dashboard/assets/drawer-Bs8MIIHc.js.gz +0 -0
  162. package/dist/dashboard/assets/entry-BRszm4_Z.js.gz +0 -0
  163. package/dist/dashboard/assets/entry-CBHzQ6Qh.js.gz +0 -0
  164. package/dist/dashboard/assets/entry-DcobaKFu.js.gz +0 -0
  165. package/dist/dashboard/assets/github-badge-BuDsrsPn.js.gz +0 -0
  166. package/dist/dashboard/assets/index-Ck4uwuYb.js.gz +0 -0
  167. package/dist/dashboard/assets/inline-DToMICwl.js.gz +0 -0
  168. package/dist/dashboard/assets/input-WlyHWeG2.js.gz +0 -0
  169. package/dist/dashboard/assets/layout-DaJ5baJr.js.gz +0 -0
  170. package/dist/dashboard/assets/list-row-B3oSK13z.js.gz +0 -0
  171. package/dist/dashboard/assets/live-ta6GMb-8.js.gz +0 -0
  172. package/dist/dashboard/assets/model-bans-BmYTmbAk.js +0 -1
  173. package/dist/dashboard/assets/model-bans-BmYTmbAk.js.gz +0 -0
  174. package/dist/dashboard/assets/mutation-g_fduiNw.js.gz +0 -0
  175. package/dist/dashboard/assets/query-keys-UETqfytp.js.gz +0 -0
  176. package/dist/dashboard/assets/routing-BPUo-W8d.js.gz +0 -0
  177. package/dist/dashboard/assets/select-BrrUDUPc.js.gz +0 -0
  178. package/dist/dashboard/assets/settings--3hxSyry.js +0 -1
  179. package/dist/dashboard/assets/settings--3hxSyry.js.gz +0 -0
  180. package/dist/dashboard/assets/stack-B1w5BM0u.js.gz +0 -0
  181. package/dist/dashboard/assets/state-DhZBfWgx.js.gz +0 -0
  182. package/dist/dashboard/assets/switch-yD7D99Lg.js.gz +0 -0
  183. package/dist/dashboard/assets/useMutation-EDFRkn1i.js.gz +0 -0
  184. package/src/console/admin/model-abuse/contracts.ts +0 -38
  185. package/src/console/admin/model-abuse/routes.ts +0 -97
  186. package/src/security/model-abuse.ts +0 -704
  187. package/src/transport/guards/concurrency-guard.ts +0 -125
  188. /package/dist/dashboard/assets/{app-MdVfwraB.js → app-CaXZ9SCn.js} +0 -0
  189. /package/dist/dashboard/assets/{app-MdVfwraB.js.gz → app-CaXZ9SCn.js.gz} +0 -0
@@ -1,391 +1,381 @@
1
- import { Elysia } from "elysia";
2
- import { ConsoleDomainError } from "./shared/errors";
3
- import { createCliToolsRoutes } from "./cli-tools/routes";
4
- import { createAccountQuotaRoutes } from "./quota/account-quota";
5
- import { createApiKeyRoutes } from "./api-keys/routes";
6
- import { familyBucketSpend } from "./api-keys/bucket-spend";
7
- import { createProviderCatalogRoutes } from "./providers/catalog/routes";
8
- import { createInferhubRoutes } from "./inferhub/routes";
9
- import { createInferhubKeyStore } from "./inferhub/key-store";
10
- import {
11
- createProviderDetailRoutes,
12
- } from "./providers/detail/routes";
13
- import {
14
- createOAuthLoginRoutes,
15
- completeLogin,
16
- DrizzleOAuthAccountStore,
17
- type OAuthLoginConfig,
18
- } from "./providers/oauth/routes";
19
- import {
20
- DrizzleProviderCatalogStore,
21
- } from "./providers/catalog/store";
22
- import { DrizzleProviderDetailStore } from "./providers/detail/store";
23
- import { createModelRoutingRoutes } from "./routing/model/routes";
24
- import { createNetworkPoolRoutes } from "./routing/pools/routes";
25
- import { DrizzleModelRoutingStore } from "./routing/model/store";
26
- import { DrizzleNetworkPoolStore } from "./routing/pools/store";
27
- import { createRuntimeSettingsRoutes } from "./settings/routes";
28
- import { createDataManagementRoutes } from "./data-management/routes";
29
- import { healthEventBuffer } from "../persistence/health-event-flush";
30
- import { countAllHealthEvents, deleteAllHealthEvents } from "../providers/operations/account-health-service";
31
- import { DrizzleRuntimeSettingsStore } from "./settings/store";
32
- import { createObservabilityRoutes } from "./observability/routes";
33
- import { DrizzleObservabilityStore } from "./observability/store";
34
- import { createLiveRoutes } from "./observability/live";
35
- import { createLogRoutes } from "./observability/logs";
36
- import { createAuditRoutes } from "./audit/routes";
37
- import { createModelAbuseRoutes } from "./admin/model-abuse/routes";
38
- import { DrizzleAuditReadStore } from "./audit/store";
39
- import { createPerformanceRoutes } from "./observability/performance";
40
- import { createUpdateRoutes } from "./updates/routes";
41
- import { createRemoteSupportRoutes } from "./remote-support/routes";
42
- import { createRemoteSupportService } from "./remote-support/service";
43
- import { resolvePort } from "../config/runtime";
44
- import { createBackupRoutes } from "./backup/routes";
45
- import { BackupService } from "./backup/service";
46
- import type { ConsoleCredentialService } from "./auth/credentials";
47
- import { resolveTelemetryRetentionDays } from "../config/telemetry";
48
- import { payloadRetentionMs, resolvePayloadCapture } from "../observability/payload-capture";
49
- import { TELEMETRY_EVENT_DIR } from "../observability/event-log";
50
-
51
- import { InMemoryOAuthFlowStore, OAuthFlowStore } from "../providers/authentication/oauth-flow-store";
52
- import { OAuthCallbackListener } from "./providers/oauth/callback-listener";
53
- import {
54
- createAccountExportCredentialsResolver,
55
- createAccountSecretResolver,
56
- } from "../providers/operations/provider-credential-service";
57
- import { syncByokProvider } from "../providers/operations/provider-catalog-service";
58
- import { DrizzleApiKeyStore } from "../persistence/stores/api-key-store";
59
- import { DrizzleShareLinkStore } from "../persistence/stores/share-store";
60
- import { createShareUsagePort } from "./share/share-usage";
61
- import { resolveSsrfPolicy } from "../config/network";
62
- import { createValidatedFetch } from "../network/outbound-fetch";
63
- import type { AuditRecorder } from "./auth/audit-recorder";
64
- import type { CliToolService } from "./cli-tools/service";
65
- import type { NetworkPoolSelector } from "../network/pool/selector";
66
- import type { LiveInFlightSource } from "./observability/live";
67
- import type { ApiKeyAdmissionService } from "../security/admission/service";
68
- import type { ModelStrikeService } from "../security/model-abuse";
69
- import type { TelemetryBatchBuffer } from "../observability/telemetry-buffer";
70
- import type { OAuthRefreshService } from "../providers/authentication/oauth-refresh-service";
71
- import { resolveRedisBackend } from "../persistence/redis";
72
- import type { RedisClient } from "../persistence/redis";
73
- import type { ProviderRegistry } from "../providers/provider-registry";
74
- import type { BundledProviderCatalog } from "../providers/operations/provider-catalog-service";
75
- import type { ValidatedNetworkBindingFactory } from "../network/pool/resolver";
76
- import type { CartethyiaDatabase } from "../persistence/db-handle";
77
- import type { ConsoleAccessResolver } from "./auth/access";
78
-
79
- export interface ConsoleDomainContext {
80
- readonly db: CartethyiaDatabase;
81
- readonly accessResolver: ConsoleAccessResolver;
82
- readonly providerRegistry: ProviderRegistry;
83
- readonly bundledModelCatalog: BundledProviderCatalog;
84
- readonly networkBindingFactory: ValidatedNetworkBindingFactory;
85
- readonly oauthRefreshService: OAuthRefreshService;
86
- readonly auditRecorder: AuditRecorder;
87
- readonly cliToolService: CliToolService;
88
- readonly redis: RedisClient | undefined;
89
- /** Drops the cached route snapshot after a catalog write. Structurally
90
- * satisfied by `RouteSnapshotService`. */
91
- readonly routeSnapshotService: { invalidate(): Promise<number> };
92
- readonly poolSelector: NetworkPoolSelector;
93
- readonly telemetryBuffer: TelemetryBatchBuffer;
94
- /**
95
- * Queued writers the console can drain on demand. Optional because a flush
96
- * is only meaningful when the process owns the buffers; a caller that does
97
- * not pass them simply loses the button rather than failing to boot.
98
- */
99
- readonly flushBuffers?: {
100
- flush(): Promise<void>;
101
- };
102
- /** Owns the live in-flight gauge the console surfaces. */
103
- readonly stateStore?: LiveInFlightSource;
104
- readonly admissionService: Pick<ApiKeyAdmissionService, "purgeKey">;
105
- /** Graduated model-abuse strikes: list and lift bans from the console. */
106
- readonly modelStrikes?: Pick<ModelStrikeService, "listBans" | "unban" | "ban">;
107
- readonly readRoutingAccountInflight?:
108
- | ((
109
- providerId: string,
110
- tenantId: string | null,
111
- ) => Promise<readonly { accountId: string; inflight: number }[]>)
112
- | undefined;
113
- /**
114
- * Loads the signed-in console user, for surfaces that re-authenticate the
115
- * operator rather than trusting the session alone (backup export/restore).
116
- */
117
- readonly loadConsoleUser: (request: Request) => Promise<{ readonly passwordHash: string } | null>;
118
- /** Password verification against the console user's stored hash. */
119
- readonly credentialService: Pick<ConsoleCredentialService, "verifyPassword">;
120
- }
121
- import { and, eq, isNull, or } from "drizzle-orm";
122
- import { networkPools, providerRoutingSettings } from "../persistence/schema";
123
- import { resolveTenantOverride } from "../persistence/tenant-scope";
124
- import { DEFAULT_PROXY_SKIP_PROVIDER_IDS } from "../providers/provider-metadata";
125
- import type { ProbeOutboundBinding } from "../providers/discovery/probing-service";
126
-
127
-
128
- export function registerConsoleDomains(
129
- console: Elysia<any, any, any, any, any, any, any, any>,
130
- ctx: ConsoleDomainContext,
131
- ): void {
132
- const observabilityStore = new DrizzleObservabilityStore(ctx.db, ctx.redis);
133
- const auditReadStore = new DrizzleAuditReadStore(ctx.db);
134
- /** Refresh-aware credential resolution for quota routes. */
135
- const resolveCredential = createAccountSecretResolver({
136
- db: ctx.db,
137
- resolveRefresher: (id) => ctx.providerRegistry.resolveRefresher(id),
138
- refreshService: ctx.oauthRefreshService,
139
- });
140
- const resolveExportCredentials = createAccountExportCredentialsResolver({
141
- db: ctx.db,
142
- resolveRefresher: (id) => ctx.providerRegistry.resolveRefresher(id),
143
- refreshService: ctx.oauthRefreshService,
144
- });
145
- const providerCatalogStore = new DrizzleProviderCatalogStore(ctx.db, {
146
- telemetryBuffer: ctx.telemetryBuffer,
147
- bundledModelCatalog: ctx.bundledModelCatalog,
148
- providerRegistry: ctx.providerRegistry,
149
- outboundFetchFor: async (tenantId: string, providerId?: string): Promise<ProbeOutboundBinding> => {
150
- let bypass = false;
151
- if (providerId) {
152
- const rows = await ctx.db
153
- .select({
154
- tenantId: providerRoutingSettings.tenantId,
155
- skipProxy: providerRoutingSettings.skipProxy,
156
- })
157
- .from(providerRoutingSettings)
158
- .where(
159
- and(
160
- eq(providerRoutingSettings.providerId, providerId),
161
- or(
162
- eq(providerRoutingSettings.tenantId, tenantId),
163
- isNull(providerRoutingSettings.tenantId),
164
- ),
165
- ),
166
- );
167
- const tenantSetting = rows.find((row) => row.tenantId === tenantId)?.skipProxy;
168
- const globalSetting = rows.find((row) => row.tenantId === null)?.skipProxy;
169
- bypass = resolveTenantOverride(
170
- tenantSetting,
171
- globalSetting,
172
- DEFAULT_PROXY_SKIP_PROVIDER_IDS.has(providerId),
173
- );
174
- }
175
-
176
- if (!bypass) {
177
- const activePools = await ctx.db
178
- .select({
179
- id: networkPools.id,
180
- maxInflight: networkPools.maxInflight,
181
- weight: networkPools.weight,
182
- })
183
- .from(networkPools)
184
- .where(and(eq(networkPools.tenantId, tenantId), eq(networkPools.status, "active")));
185
-
186
- if (activePools.length > 0) {
187
- const limits: Record<string, number> = {};
188
- const weights: Record<string, number> = {};
189
- for (const p of activePools) {
190
- limits[p.id] = p.maxInflight ?? 10;
191
- weights[p.id] = p.weight ?? 100;
192
- }
193
- const slot = await ctx.poolSelector.tryAcquireAvailablePool(
194
- activePools.map((p) => p.id),
195
- providerId ?? "default",
196
- limits,
197
- weights,
198
- );
199
- if (slot) {
200
- return {
201
- fetch: ctx.networkBindingFactory.fetch(slot.poolId, tenantId),
202
- http2Bidi: ctx.networkBindingFactory.http2Bidi(slot.poolId, tenantId),
203
- networkPoolId: slot.poolId,
204
- release: slot.release,
205
- };
206
- }
207
- // Every pool is saturated or cooling down: fail closed with an
208
- // explicit 503 rather than pinning the first pool and bypassing its
209
- // capacity accounting and cooldown.
210
- throw new ConsoleDomainError(
211
- "pool_unavailable",
212
- 503,
213
- `All network pools are saturated for provider ${providerId ?? "default"}`,
214
- );
215
- }
216
- }
217
-
218
- return {
219
- fetch: ctx.networkBindingFactory.fetch(undefined, tenantId),
220
- http2Bidi: ctx.networkBindingFactory.http2Bidi(undefined, tenantId),
221
- };
222
- },
223
- snapshotInvalidator: ctx.routeSnapshotService,
224
- readAccountInflight: async (providerId, tenantId) =>
225
- ctx.readRoutingAccountInflight ? ctx.readRoutingAccountInflight(providerId, tenantId) : [],
226
- });
227
- const providerDetailStore = new DrizzleProviderDetailStore(ctx.db);
228
- const networkPoolStore = new DrizzleNetworkPoolStore(ctx.db, resolveSsrfPolicy());
229
- const runtimeSettingsStore = new DrizzleRuntimeSettingsStore(ctx.db, resolveRedisBackend(ctx.redis));
230
- const modelRoutingStore = new DrizzleModelRoutingStore(ctx.db);
231
- const apiKeyStore = new DrizzleApiKeyStore(ctx.db);
232
- const shareStore = new DrizzleShareLinkStore(ctx.db);
233
- const shareActivity = createShareUsagePort({ db: ctx.db, redis: ctx.redis });
234
-
235
-
236
- console.use(createObservabilityRoutes({ store: observabilityStore, accessResolver: ctx.accessResolver }));
237
- console.use(createLiveRoutes({
238
- accessResolver: ctx.accessResolver,
239
- poolSelector: ctx.poolSelector,
240
- db: ctx.db,
241
- ...(ctx.stateStore ? { stateStore: ctx.stateStore } : {}),
242
- }));
243
- console.use(createLogRoutes({ accessResolver: ctx.accessResolver, auditSink: ctx.auditRecorder }));
244
- console.use(createPerformanceRoutes({ accessResolver: ctx.accessResolver }));
245
- console.use(createUpdateRoutes({
246
- accessResolver: ctx.accessResolver,
247
- observabilityStore,
248
- }));
249
- console.use(createRemoteSupportRoutes({
250
- accessResolver: ctx.accessResolver,
251
- service: createRemoteSupportService({ port: resolvePort() }),
252
- }));
253
- console.use(createAuditRoutes({ store: auditReadStore, accessResolver: ctx.accessResolver }));
254
- // InferHub market data. On demand only: a route fetches when its snapshot is
255
- // past its freshness window, so a page nobody opens causes no upstream calls.
256
- const inferhubKeys = createInferhubKeyStore(ctx.db);
257
- console.use(createInferhubRoutes({
258
- accessResolver: ctx.accessResolver,
259
- resolveConfigKey: () => inferhubKeys.read(),
260
- keyStore: inferhubKeys,
261
- }));
262
- if (ctx.modelStrikes) {
263
- console.use(createModelAbuseRoutes({
264
- strikes: ctx.modelStrikes,
265
- accessResolver: ctx.accessResolver,
266
- }));
267
- }
268
- console.use(createProviderCatalogRoutes({
269
- store: providerCatalogStore,
270
- accessResolver: ctx.accessResolver,
271
- auditSink: ctx.auditRecorder,
272
- providerRegistry: ctx.providerRegistry,
273
- snapshotInvalidator: ctx.routeSnapshotService,
274
- resolveExportCredentials,
275
- // Without this, a custom provider only becomes dispatchable after a
276
- // restart: boot registers BYOK rows once, and the catalog routes are the
277
- syncByokProvider: (providerId) =>
278
- syncByokProvider(ctx.providerRegistry, ctx.db, providerId, resolveSsrfPolicy()),
279
- listRoutingTargets: async (tenantId) => ({
280
- aliases: await modelRoutingStore.listAliases(tenantId),
281
- combos: await modelRoutingStore.listCombos(tenantId),
282
- }),
283
- }));
284
- console.use(createProviderDetailRoutes({
285
- store: providerDetailStore,
286
- accessResolver: ctx.accessResolver,
287
- auditSink: ctx.auditRecorder,
288
- snapshotInvalidator: ctx.routeSnapshotService,
289
- accountInflight: ctx.readRoutingAccountInflight,
290
- }));
291
- console.use(createNetworkPoolRoutes({
292
- store: networkPoolStore,
293
- accessResolver: ctx.accessResolver,
294
- auditSink: ctx.auditRecorder,
295
- poolSelector: ctx.poolSelector,
296
- snapshotInvalidator: ctx.routeSnapshotService,
297
- // Hosted-relay deploys reach Cloudflare/Vercel/Deno APIs, so they use the
298
- // same SSRF-validated fetch as every other egress.
299
- relayFetch: createValidatedFetch({ policy: resolveSsrfPolicy() }),
300
- }));
301
- console.use(createRuntimeSettingsRoutes({ store: runtimeSettingsStore, accessResolver: ctx.accessResolver, auditSink: ctx.auditRecorder, snapshotInvalidator: ctx.routeSnapshotService }));
302
- console.use(
303
- createDataManagementRoutes({
304
- telemetryDir: TELEMETRY_EVENT_DIR,
305
- retentionDays: resolveTelemetryRetentionDays(),
306
- /*
307
- * Resolved per tenant, not once at boot: the Request detail window is a
308
- * per-tenant setting, and this panel is the only place that reports the
309
- * number back. A deployment-level value made it show the default while
310
- * the gateway applied the tenant's own window.
311
- *
312
- * It goes through the same `resolvePayloadCapture` the dispatch path
313
- * uses, so the panel and the gateway cannot disagree about how long a
314
- * frame lives.
315
- */
316
- payloadRetentionMs: async (tenantId) => {
317
- const resolved = await resolvePayloadCapture(ctx.db, tenantId);
318
- return resolved.ok ? resolved.retentionMs : payloadRetentionMs();
319
- },
320
- accessResolver: ctx.accessResolver,
321
- auditSink: ctx.auditRecorder,
322
- flusher: ctx.flushBuffers,
323
- // The console's one row-level purge: the health & error log is a
324
- // diagnostic trail no request-path code reads. Dropping the buffered
325
- // writes before the DELETE is what makes it sound — otherwise a batch
326
- // in flight would land behind the purge and resurrect history that was
327
- // just counted as deleted.
328
- healthEvents: {
329
- count: () => countAllHealthEvents(ctx.db),
330
- purgeAll: async () => {
331
- await healthEventBuffer()?.drain();
332
- return await deleteAllHealthEvents(ctx.db);
333
- },
334
- },
335
- }),
336
- );
337
- console.use(
338
- createBackupRoutes({
339
- accessResolver: ctx.accessResolver,
340
- // The service is built per request: re-authenticating the operator needs
341
- // the request's session, and holding a request-bound closure in a
342
- // long-lived object would leak one request's identity into the next.
343
- backupFor: (request: Request) =>
344
- new BackupService({
345
- db: ctx.db,
346
- verifyPassword: async (password: string) => {
347
- const user = await ctx.loadConsoleUser(request);
348
- if (user === null) return false;
349
- return ctx.credentialService.verifyPassword(password, user.passwordHash);
350
- },
351
- }),
352
- snapshotInvalidator: ctx.routeSnapshotService,
353
- apiKeyStore,
354
- admissionService: ctx.admissionService,
355
- auditSink: ctx.auditRecorder,
356
- }),
357
- );
358
- console.use(createModelRoutingRoutes({ store: modelRoutingStore, accessResolver: ctx.accessResolver, auditSink: ctx.auditRecorder, snapshotInvalidator: ctx.routeSnapshotService }));
359
- console.use(createApiKeyRoutes({ store: apiKeyStore, accessResolver: ctx.accessResolver, auditSink: ctx.auditRecorder, shareStore, shareActivity, admissionService: ctx.admissionService, bucketSpend: (keyId, now) => familyBucketSpend(ctx.db, keyId, now) }));
360
- console.use(createCliToolsRoutes({ service: ctx.cliToolService, accessResolver: ctx.accessResolver, auditSink: ctx.auditRecorder, snapshotInvalidator: ctx.routeSnapshotService }));
361
- console.use(createAccountQuotaRoutes({
362
- db: ctx.db,
363
- accessResolver: ctx.accessResolver,
364
- auditRecorder: ctx.auditRecorder,
365
- redis: ctx.redis,
366
- snapshotInvalidator: ctx.routeSnapshotService,
367
- providerRegistry: ctx.providerRegistry,
368
- resolveCredential,
369
- poolSelector: ctx.poolSelector,
370
- networkBindingFactory: ctx.networkBindingFactory,
371
- }));
372
- // The listener binds the loopback port a browser redirect names, so the
373
- // callback delivers itself instead of waiting for a manual paste. One
374
- // instance per console: a port is shared by every flow registered on it, and
375
- // it is released when the last of those flows settles.
376
- const oauthCallbackListener = new OAuthCallbackListener({
377
- completer: {
378
- complete: (providerId, code, state) =>
379
- completeLogin(oauthConfig, providerId, code, state),
380
- },
381
- });
382
- const oauthConfig: OAuthLoginConfig = {
383
- providerRegistry: ctx.providerRegistry,
384
- oauthFlowStore: ctx.redis ? new OAuthFlowStore(ctx.redis) : new InMemoryOAuthFlowStore(),
385
- accountStore: new DrizzleOAuthAccountStore(ctx.db),
386
- accessResolver: ctx.accessResolver,
387
- snapshotInvalidator: ctx.routeSnapshotService,
388
- callbackListener: oauthCallbackListener,
389
- };
390
- console.use(createOAuthLoginRoutes(oauthConfig));
391
- }
1
+ import { Elysia } from "elysia";
2
+ import { ConsoleDomainError } from "./shared/errors";
3
+ import { createCliToolsRoutes } from "./cli-tools/routes";
4
+ import { createAccountQuotaRoutes } from "./quota/account-quota";
5
+ import { createApiKeyRoutes } from "./api-keys/routes";
6
+ import { familyBucketSpend } from "./api-keys/bucket-spend";
7
+ import { createProviderCatalogRoutes } from "./providers/catalog/routes";
8
+ import { createInferhubRoutes } from "./inferhub/routes";
9
+ import { createInferhubKeyStore } from "./inferhub/key-store";
10
+ import {
11
+ createProviderDetailRoutes,
12
+ } from "./providers/detail/routes";
13
+ import {
14
+ createOAuthLoginRoutes,
15
+ completeLogin,
16
+ DrizzleOAuthAccountStore,
17
+ type OAuthLoginConfig,
18
+ } from "./providers/oauth/routes";
19
+ import {
20
+ DrizzleProviderCatalogStore,
21
+ } from "./providers/catalog/store";
22
+ import { DrizzleProviderDetailStore } from "./providers/detail/store";
23
+ import { createModelRoutingRoutes } from "./routing/model/routes";
24
+ import { createNetworkPoolRoutes } from "./routing/pools/routes";
25
+ import { DrizzleModelRoutingStore } from "./routing/model/store";
26
+ import { DrizzleNetworkPoolStore } from "./routing/pools/store";
27
+ import { createRuntimeSettingsRoutes } from "./settings/routes";
28
+ import { createDataManagementRoutes } from "./data-management/routes";
29
+ import { healthEventBuffer } from "../persistence/health-event-flush";
30
+ import { countAllHealthEvents, deleteAllHealthEvents } from "../providers/operations/account-health-service";
31
+ import { DrizzleRuntimeSettingsStore } from "./settings/store";
32
+ import { createObservabilityRoutes } from "./observability/routes";
33
+ import { DrizzleObservabilityStore } from "./observability/store";
34
+ import { createLiveRoutes } from "./observability/live";
35
+ import { createLogRoutes } from "./observability/logs";
36
+ import { createAuditRoutes } from "./audit/routes";
37
+ import { DrizzleAuditReadStore } from "./audit/store";
38
+ import { createPerformanceRoutes } from "./observability/performance";
39
+ import { createUpdateRoutes } from "./updates/routes";
40
+ import { createRemoteSupportRoutes } from "./remote-support/routes";
41
+ import { createRemoteSupportService } from "./remote-support/service";
42
+ import { resolvePort } from "../config/runtime";
43
+ import { createBackupRoutes } from "./backup/routes";
44
+ import { BackupService } from "./backup/service";
45
+ import type { ConsoleCredentialService } from "./auth/credentials";
46
+ import { resolveTelemetryRetentionDays } from "../config/telemetry";
47
+ import { payloadRetentionMs, resolvePayloadCapture } from "../observability/payload-capture";
48
+ import { TELEMETRY_EVENT_DIR } from "../observability/event-log";
49
+
50
+ import { InMemoryOAuthFlowStore, OAuthFlowStore } from "../providers/authentication/oauth-flow-store";
51
+ import { OAuthCallbackListener } from "./providers/oauth/callback-listener";
52
+ import {
53
+ createAccountExportCredentialsResolver,
54
+ createAccountSecretResolver,
55
+ } from "../providers/operations/provider-credential-service";
56
+ import { syncByokProvider } from "../providers/operations/provider-catalog-service";
57
+ import { DrizzleApiKeyStore } from "../persistence/stores/api-key-store";
58
+ import { DrizzleShareLinkStore } from "../persistence/stores/share-store";
59
+ import { createShareUsagePort } from "./share/share-usage";
60
+ import { resolveSsrfPolicy } from "../config/network";
61
+ import { createValidatedFetch } from "../network/outbound-fetch";
62
+ import type { AuditRecorder } from "./auth/audit-recorder";
63
+ import type { CliToolService } from "./cli-tools/service";
64
+ import type { NetworkPoolSelector } from "../network/pool/selector";
65
+ import type { LiveInFlightSource } from "./observability/live";
66
+ import type { ApiKeyAdmissionService } from "../security/admission/service";
67
+ import type { TelemetryBatchBuffer } from "../observability/telemetry-buffer";
68
+ import type { OAuthRefreshService } from "../providers/authentication/oauth-refresh-service";
69
+ import { resolveRedisBackend } from "../persistence/redis";
70
+ import type { RedisClient } from "../persistence/redis";
71
+ import type { ProviderRegistry } from "../providers/provider-registry";
72
+ import type { BundledProviderCatalog } from "../providers/operations/provider-catalog-service";
73
+ import type { ValidatedNetworkBindingFactory } from "../network/pool/resolver";
74
+ import type { CartethyiaDatabase } from "../persistence/db-handle";
75
+ import type { ConsoleAccessResolver } from "./auth/access";
76
+
77
+ export interface ConsoleDomainContext {
78
+ readonly db: CartethyiaDatabase;
79
+ readonly accessResolver: ConsoleAccessResolver;
80
+ readonly providerRegistry: ProviderRegistry;
81
+ readonly bundledModelCatalog: BundledProviderCatalog;
82
+ readonly networkBindingFactory: ValidatedNetworkBindingFactory;
83
+ readonly oauthRefreshService: OAuthRefreshService;
84
+ readonly auditRecorder: AuditRecorder;
85
+ readonly cliToolService: CliToolService;
86
+ readonly redis: RedisClient | undefined;
87
+ /** Drops the cached route snapshot after a catalog write. Structurally
88
+ * satisfied by `RouteSnapshotService`. */
89
+ readonly routeSnapshotService: { invalidate(): Promise<number> };
90
+ readonly poolSelector: NetworkPoolSelector;
91
+ readonly telemetryBuffer: TelemetryBatchBuffer;
92
+ /**
93
+ * Queued writers the console can drain on demand. Optional because a flush
94
+ * is only meaningful when the process owns the buffers; a caller that does
95
+ * not pass them simply loses the button rather than failing to boot.
96
+ */
97
+ readonly flushBuffers?: {
98
+ flush(): Promise<void>;
99
+ };
100
+ /** Owns the live in-flight gauge the console surfaces. */
101
+ readonly stateStore?: LiveInFlightSource;
102
+ readonly admissionService: Pick<ApiKeyAdmissionService, "purgeKey">;
103
+ readonly readRoutingAccountInflight?:
104
+ | ((
105
+ providerId: string,
106
+ tenantId: string | null,
107
+ ) => Promise<readonly { accountId: string; inflight: number }[]>)
108
+ | undefined;
109
+ /**
110
+ * Loads the signed-in console user, for surfaces that re-authenticate the
111
+ * operator rather than trusting the session alone (backup export/restore).
112
+ */
113
+ readonly loadConsoleUser: (request: Request) => Promise<{ readonly passwordHash: string } | null>;
114
+ /** Password verification against the console user's stored hash. */
115
+ readonly credentialService: Pick<ConsoleCredentialService, "verifyPassword">;
116
+ }
117
+ import { and, eq, isNull, or } from "drizzle-orm";
118
+ import { networkPools, providerRoutingSettings } from "../persistence/schema";
119
+ import { resolveTenantOverride } from "../persistence/tenant-scope";
120
+ import { DEFAULT_PROXY_SKIP_PROVIDER_IDS } from "../providers/provider-metadata";
121
+ import type { ProbeOutboundBinding } from "../providers/discovery/probing-service";
122
+
123
+
124
+ export function registerConsoleDomains(
125
+ console: Elysia<any, any, any, any, any, any, any, any>,
126
+ ctx: ConsoleDomainContext,
127
+ ): void {
128
+ const observabilityStore = new DrizzleObservabilityStore(ctx.db, ctx.redis);
129
+ const auditReadStore = new DrizzleAuditReadStore(ctx.db);
130
+ /** Refresh-aware credential resolution for quota routes. */
131
+ const resolveCredential = createAccountSecretResolver({
132
+ db: ctx.db,
133
+ resolveRefresher: (id) => ctx.providerRegistry.resolveRefresher(id),
134
+ refreshService: ctx.oauthRefreshService,
135
+ });
136
+ const resolveExportCredentials = createAccountExportCredentialsResolver({
137
+ db: ctx.db,
138
+ resolveRefresher: (id) => ctx.providerRegistry.resolveRefresher(id),
139
+ refreshService: ctx.oauthRefreshService,
140
+ });
141
+ const providerCatalogStore = new DrizzleProviderCatalogStore(ctx.db, {
142
+ telemetryBuffer: ctx.telemetryBuffer,
143
+ bundledModelCatalog: ctx.bundledModelCatalog,
144
+ providerRegistry: ctx.providerRegistry,
145
+ outboundFetchFor: async (tenantId: string, providerId?: string): Promise<ProbeOutboundBinding> => {
146
+ let bypass = false;
147
+ if (providerId) {
148
+ const rows = await ctx.db
149
+ .select({
150
+ tenantId: providerRoutingSettings.tenantId,
151
+ skipProxy: providerRoutingSettings.skipProxy,
152
+ })
153
+ .from(providerRoutingSettings)
154
+ .where(
155
+ and(
156
+ eq(providerRoutingSettings.providerId, providerId),
157
+ or(
158
+ eq(providerRoutingSettings.tenantId, tenantId),
159
+ isNull(providerRoutingSettings.tenantId),
160
+ ),
161
+ ),
162
+ );
163
+ const tenantSetting = rows.find((row) => row.tenantId === tenantId)?.skipProxy;
164
+ const globalSetting = rows.find((row) => row.tenantId === null)?.skipProxy;
165
+ bypass = resolveTenantOverride(
166
+ tenantSetting,
167
+ globalSetting,
168
+ DEFAULT_PROXY_SKIP_PROVIDER_IDS.has(providerId),
169
+ );
170
+ }
171
+
172
+ if (!bypass) {
173
+ const activePools = await ctx.db
174
+ .select({
175
+ id: networkPools.id,
176
+ maxInflight: networkPools.maxInflight,
177
+ weight: networkPools.weight,
178
+ })
179
+ .from(networkPools)
180
+ .where(and(eq(networkPools.tenantId, tenantId), eq(networkPools.status, "active")));
181
+
182
+ if (activePools.length > 0) {
183
+ const limits: Record<string, number> = {};
184
+ const weights: Record<string, number> = {};
185
+ for (const p of activePools) {
186
+ limits[p.id] = p.maxInflight ?? 10;
187
+ weights[p.id] = p.weight ?? 100;
188
+ }
189
+ const slot = await ctx.poolSelector.tryAcquireAvailablePool(
190
+ activePools.map((p) => p.id),
191
+ providerId ?? "default",
192
+ limits,
193
+ weights,
194
+ );
195
+ if (slot) {
196
+ return {
197
+ fetch: ctx.networkBindingFactory.fetch(slot.poolId, tenantId),
198
+ http2Bidi: ctx.networkBindingFactory.http2Bidi(slot.poolId, tenantId),
199
+ networkPoolId: slot.poolId,
200
+ release: slot.release,
201
+ };
202
+ }
203
+ // Every pool is saturated or cooling down: fail closed with an
204
+ // explicit 503 rather than pinning the first pool and bypassing its
205
+ // capacity accounting and cooldown.
206
+ throw new ConsoleDomainError(
207
+ "pool_unavailable",
208
+ 503,
209
+ `All network pools are saturated for provider ${providerId ?? "default"}`,
210
+ );
211
+ }
212
+ }
213
+
214
+ return {
215
+ fetch: ctx.networkBindingFactory.fetch(undefined, tenantId),
216
+ http2Bidi: ctx.networkBindingFactory.http2Bidi(undefined, tenantId),
217
+ };
218
+ },
219
+ snapshotInvalidator: ctx.routeSnapshotService,
220
+ readAccountInflight: async (providerId, tenantId) =>
221
+ ctx.readRoutingAccountInflight ? ctx.readRoutingAccountInflight(providerId, tenantId) : [],
222
+ });
223
+ const providerDetailStore = new DrizzleProviderDetailStore(ctx.db);
224
+ const networkPoolStore = new DrizzleNetworkPoolStore(ctx.db, resolveSsrfPolicy());
225
+ const runtimeSettingsStore = new DrizzleRuntimeSettingsStore(ctx.db, resolveRedisBackend(ctx.redis));
226
+ const modelRoutingStore = new DrizzleModelRoutingStore(ctx.db);
227
+ const apiKeyStore = new DrizzleApiKeyStore(ctx.db);
228
+ const shareStore = new DrizzleShareLinkStore(ctx.db);
229
+ const shareActivity = createShareUsagePort({ db: ctx.db, redis: ctx.redis });
230
+
231
+
232
+ console.use(createObservabilityRoutes({ store: observabilityStore, accessResolver: ctx.accessResolver }));
233
+ console.use(createLiveRoutes({
234
+ accessResolver: ctx.accessResolver,
235
+ poolSelector: ctx.poolSelector,
236
+ db: ctx.db,
237
+ ...(ctx.stateStore ? { stateStore: ctx.stateStore } : {}),
238
+ }));
239
+ console.use(createLogRoutes({ accessResolver: ctx.accessResolver, auditSink: ctx.auditRecorder }));
240
+ console.use(createPerformanceRoutes({ accessResolver: ctx.accessResolver }));
241
+ console.use(createUpdateRoutes({
242
+ accessResolver: ctx.accessResolver,
243
+ observabilityStore,
244
+ }));
245
+ console.use(createRemoteSupportRoutes({
246
+ accessResolver: ctx.accessResolver,
247
+ service: createRemoteSupportService({ port: resolvePort() }),
248
+ }));
249
+ console.use(createAuditRoutes({ store: auditReadStore, accessResolver: ctx.accessResolver }));
250
+ // InferHub market data. On demand only: a route fetches when its snapshot is
251
+ // past its freshness window, so a page nobody opens causes no upstream calls.
252
+ const inferhubKeys = createInferhubKeyStore(ctx.db);
253
+ console.use(createInferhubRoutes({
254
+ accessResolver: ctx.accessResolver,
255
+ resolveConfigKey: () => inferhubKeys.read(),
256
+ keyStore: inferhubKeys,
257
+ }));
258
+ console.use(createProviderCatalogRoutes({
259
+ store: providerCatalogStore,
260
+ accessResolver: ctx.accessResolver,
261
+ auditSink: ctx.auditRecorder,
262
+ providerRegistry: ctx.providerRegistry,
263
+ snapshotInvalidator: ctx.routeSnapshotService,
264
+ resolveExportCredentials,
265
+ // Without this, a custom provider only becomes dispatchable after a
266
+ // restart: boot registers BYOK rows once, and the catalog routes are the
267
+ syncByokProvider: (providerId) =>
268
+ syncByokProvider(ctx.providerRegistry, ctx.db, providerId, resolveSsrfPolicy()),
269
+ listRoutingTargets: async (tenantId) => ({
270
+ aliases: await modelRoutingStore.listAliases(tenantId),
271
+ combos: await modelRoutingStore.listCombos(tenantId),
272
+ }),
273
+ }));
274
+ console.use(createProviderDetailRoutes({
275
+ store: providerDetailStore,
276
+ accessResolver: ctx.accessResolver,
277
+ auditSink: ctx.auditRecorder,
278
+ snapshotInvalidator: ctx.routeSnapshotService,
279
+ accountInflight: ctx.readRoutingAccountInflight,
280
+ }));
281
+ console.use(createNetworkPoolRoutes({
282
+ store: networkPoolStore,
283
+ accessResolver: ctx.accessResolver,
284
+ auditSink: ctx.auditRecorder,
285
+ poolSelector: ctx.poolSelector,
286
+ snapshotInvalidator: ctx.routeSnapshotService,
287
+ // Hosted-relay deploys reach Cloudflare/Vercel/Deno APIs, so they use the
288
+ // same SSRF-validated fetch as every other egress.
289
+ relayFetch: createValidatedFetch({ policy: resolveSsrfPolicy() }),
290
+ }));
291
+ console.use(createRuntimeSettingsRoutes({ store: runtimeSettingsStore, accessResolver: ctx.accessResolver, auditSink: ctx.auditRecorder, snapshotInvalidator: ctx.routeSnapshotService }));
292
+ console.use(
293
+ createDataManagementRoutes({
294
+ telemetryDir: TELEMETRY_EVENT_DIR,
295
+ retentionDays: resolveTelemetryRetentionDays(),
296
+ /*
297
+ * Resolved per tenant, not once at boot: the Request detail window is a
298
+ * per-tenant setting, and this panel is the only place that reports the
299
+ * number back. A deployment-level value made it show the default while
300
+ * the gateway applied the tenant's own window.
301
+ *
302
+ * It goes through the same `resolvePayloadCapture` the dispatch path
303
+ * uses, so the panel and the gateway cannot disagree about how long a
304
+ * frame lives.
305
+ */
306
+ payloadRetentionMs: async (tenantId) => {
307
+ const resolved = await resolvePayloadCapture(ctx.db, tenantId);
308
+ return resolved.ok ? resolved.retentionMs : payloadRetentionMs();
309
+ },
310
+ accessResolver: ctx.accessResolver,
311
+ auditSink: ctx.auditRecorder,
312
+ flusher: ctx.flushBuffers,
313
+ // The console's one row-level purge: the health & error log is a
314
+ // diagnostic trail no request-path code reads. Dropping the buffered
315
+ // writes before the DELETE is what makes it sound — otherwise a batch
316
+ // in flight would land behind the purge and resurrect history that was
317
+ // just counted as deleted.
318
+ healthEvents: {
319
+ count: () => countAllHealthEvents(ctx.db),
320
+ purgeAll: async () => {
321
+ await healthEventBuffer()?.drain();
322
+ return await deleteAllHealthEvents(ctx.db);
323
+ },
324
+ },
325
+ }),
326
+ );
327
+ console.use(
328
+ createBackupRoutes({
329
+ accessResolver: ctx.accessResolver,
330
+ // The service is built per request: re-authenticating the operator needs
331
+ // the request's session, and holding a request-bound closure in a
332
+ // long-lived object would leak one request's identity into the next.
333
+ backupFor: (request: Request) =>
334
+ new BackupService({
335
+ db: ctx.db,
336
+ verifyPassword: async (password: string) => {
337
+ const user = await ctx.loadConsoleUser(request);
338
+ if (user === null) return false;
339
+ return ctx.credentialService.verifyPassword(password, user.passwordHash);
340
+ },
341
+ }),
342
+ snapshotInvalidator: ctx.routeSnapshotService,
343
+ apiKeyStore,
344
+ admissionService: ctx.admissionService,
345
+ auditSink: ctx.auditRecorder,
346
+ }),
347
+ );
348
+ console.use(createModelRoutingRoutes({ store: modelRoutingStore, accessResolver: ctx.accessResolver, auditSink: ctx.auditRecorder, snapshotInvalidator: ctx.routeSnapshotService }));
349
+ console.use(createApiKeyRoutes({ store: apiKeyStore, accessResolver: ctx.accessResolver, auditSink: ctx.auditRecorder, shareStore, shareActivity, admissionService: ctx.admissionService, bucketSpend: (keyId, now) => familyBucketSpend(ctx.db, keyId, now) }));
350
+ console.use(createCliToolsRoutes({ service: ctx.cliToolService, accessResolver: ctx.accessResolver, auditSink: ctx.auditRecorder, snapshotInvalidator: ctx.routeSnapshotService }));
351
+ console.use(createAccountQuotaRoutes({
352
+ db: ctx.db,
353
+ accessResolver: ctx.accessResolver,
354
+ auditRecorder: ctx.auditRecorder,
355
+ redis: ctx.redis,
356
+ snapshotInvalidator: ctx.routeSnapshotService,
357
+ providerRegistry: ctx.providerRegistry,
358
+ resolveCredential,
359
+ poolSelector: ctx.poolSelector,
360
+ networkBindingFactory: ctx.networkBindingFactory,
361
+ }));
362
+ // The listener binds the loopback port a browser redirect names, so the
363
+ // callback delivers itself instead of waiting for a manual paste. One
364
+ // instance per console: a port is shared by every flow registered on it, and
365
+ // it is released when the last of those flows settles.
366
+ const oauthCallbackListener = new OAuthCallbackListener({
367
+ completer: {
368
+ complete: (providerId, code, state) =>
369
+ completeLogin(oauthConfig, providerId, code, state),
370
+ },
371
+ });
372
+ const oauthConfig: OAuthLoginConfig = {
373
+ providerRegistry: ctx.providerRegistry,
374
+ oauthFlowStore: ctx.redis ? new OAuthFlowStore(ctx.redis) : new InMemoryOAuthFlowStore(),
375
+ accountStore: new DrizzleOAuthAccountStore(ctx.db),
376
+ accessResolver: ctx.accessResolver,
377
+ snapshotInvalidator: ctx.routeSnapshotService,
378
+ callbackListener: oauthCallbackListener,
379
+ };
380
+ console.use(createOAuthLoginRoutes(oauthConfig));
381
+ }