cartethyia 2.6.7 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (187) hide show
  1. package/.env.example +106 -113
  2. package/dist/dashboard/assets/{BannedPage-Dskhyp0l.js → BannedPage-P76QLGJA.js} +1 -1
  3. package/dist/dashboard/assets/BannedPage-P76QLGJA.js.gz +0 -0
  4. package/dist/dashboard/assets/{CliToolDetailPage--SJhk7qQ.js → CliToolDetailPage-DdBggJS9.js} +1 -1
  5. package/dist/dashboard/assets/CliToolDetailPage-DdBggJS9.js.gz +0 -0
  6. package/dist/dashboard/assets/{CliToolsPage-C203VT-c.js → CliToolsPage-aN1iXhhP.js} +1 -1
  7. package/dist/dashboard/assets/CliToolsPage-aN1iXhhP.js.gz +0 -0
  8. package/dist/dashboard/assets/{CombosPage-BcS7kGG1.js → CombosPage-DE-F1nxZ.js} +1 -1
  9. package/dist/dashboard/assets/CombosPage-DE-F1nxZ.js.gz +0 -0
  10. package/dist/dashboard/assets/{ConsoleLogPage-ChmiSLwO.js → ConsoleLogPage-5SpmfLyw.js} +1 -1
  11. package/dist/dashboard/assets/ConsoleLogPage-5SpmfLyw.js.gz +0 -0
  12. package/dist/dashboard/assets/{CustomizationPage-uujbBYwC.js → CustomizationPage-3CUxMy2X.js} +1 -1
  13. package/dist/dashboard/assets/CustomizationPage-3CUxMy2X.js.gz +0 -0
  14. package/dist/dashboard/assets/{HealthEventsModal-fKAWd-OQ.js → HealthEventsModal-BkmJQKC0.js} +1 -1
  15. package/dist/dashboard/assets/HealthEventsModal-BkmJQKC0.js.gz +0 -0
  16. package/dist/dashboard/assets/{LoginPage-DoOUthbG.js → LoginPage-B_0FUKWc.js} +1 -1
  17. package/dist/dashboard/assets/LoginPage-B_0FUKWc.js.gz +0 -0
  18. package/dist/dashboard/assets/{ModelPicker-CA1kLs45.js → ModelPicker-DKVslqG3.js} +1 -1
  19. package/dist/dashboard/assets/ModelPicker-DKVslqG3.js.gz +0 -0
  20. package/dist/dashboard/assets/OverviewPage-D-H4MEiT.js +1 -0
  21. package/dist/dashboard/assets/OverviewPage-D-H4MEiT.js.gz +0 -0
  22. package/dist/dashboard/assets/{ProviderDetailPage-CFu1qGcT.js → ProviderDetailPage-CEJw6Guo.js} +3 -3
  23. package/dist/dashboard/assets/ProviderDetailPage-CEJw6Guo.js.gz +0 -0
  24. package/dist/dashboard/assets/{ProvidersPage-CnlJZbxF.js → ProvidersPage-CEplAR-i.js} +1 -1
  25. package/dist/dashboard/assets/ProvidersPage-CEplAR-i.js.gz +0 -0
  26. package/dist/dashboard/assets/{ProxyPage-B9mBTweh.js → ProxyPage-CAN7hy0f.js} +1 -1
  27. package/dist/dashboard/assets/ProxyPage-CAN7hy0f.js.gz +0 -0
  28. package/dist/dashboard/assets/{QuotaPage-BueSWX5D.js → QuotaPage-BjldQwIU.js} +1 -1
  29. package/dist/dashboard/assets/QuotaPage-BjldQwIU.js.gz +0 -0
  30. package/dist/dashboard/assets/SettingsPage-DUIoLRxk.js +1 -0
  31. package/dist/dashboard/assets/SettingsPage-DUIoLRxk.js.gz +0 -0
  32. package/dist/dashboard/assets/{SetupPage-DQ9oDRpi.js → SetupPage-_bCaTM0v.js} +1 -1
  33. package/dist/dashboard/assets/SetupPage-_bCaTM0v.js.gz +0 -0
  34. package/dist/dashboard/assets/{SortableList-DrKeMcGN.js → SortableList-fI7JkdIB.js} +1 -1
  35. package/dist/dashboard/assets/SortableList-fI7JkdIB.js.gz +0 -0
  36. package/dist/dashboard/assets/{StudioPage-qjuYtccc.js → StudioPage-CHVZqskZ.js} +1 -1
  37. package/dist/dashboard/assets/StudioPage-CHVZqskZ.js.gz +0 -0
  38. package/dist/dashboard/assets/UsagePage-DiFBymHj.js +56 -0
  39. package/dist/dashboard/assets/UsagePage-DiFBymHj.js.gz +0 -0
  40. package/dist/dashboard/assets/{api-keys-C23S9sgp.js → api-keys-DFY_fZU4.js} +1 -1
  41. package/dist/dashboard/assets/api-keys-DFY_fZU4.js.gz +0 -0
  42. package/dist/dashboard/assets/app-BOJ1_xUB.css +2 -0
  43. package/dist/dashboard/assets/app-BOJ1_xUB.css.gz +0 -0
  44. package/dist/dashboard/assets/{assert-DU6qrMA_.js → assert-CwdnZARt.js} +1 -1
  45. package/dist/dashboard/assets/assert-CwdnZARt.js.gz +0 -0
  46. package/dist/dashboard/assets/{badge-Bkz_-RrM.js → badge-Dr-uf0Au.js} +1 -1
  47. package/dist/dashboard/assets/badge-Dr-uf0Au.js.gz +0 -0
  48. package/dist/dashboard/assets/{banner-C-7y3VKV.js → banner-BddmMdoS.js} +1 -1
  49. package/dist/dashboard/assets/banner-BddmMdoS.js.gz +0 -0
  50. package/dist/dashboard/assets/{card-BOPBuBzD.js → card-CK0PXWlX.js} +1 -1
  51. package/dist/dashboard/assets/card-CK0PXWlX.js.gz +0 -0
  52. package/dist/dashboard/assets/{cli-tools-BaFUuFFF.js → cli-tools-CcB6mGh2.js} +1 -1
  53. package/dist/dashboard/assets/cli-tools-CcB6mGh2.js.gz +0 -0
  54. package/dist/dashboard/assets/{clipboard-button-e-Mlk0FH.js → clipboard-button-DGb2P9Yx.js} +1 -1
  55. package/dist/dashboard/assets/clipboard-button-DGb2P9Yx.js.gz +0 -0
  56. package/dist/dashboard/assets/{dialog-Iec_ywbD.js → dialog-LkfuL50u.js} +1 -1
  57. package/dist/dashboard/assets/dialog-LkfuL50u.js.gz +0 -0
  58. package/dist/dashboard/assets/{drawer-Bs8MIIHc.js → drawer-ZbEZ_84c.js} +1 -1
  59. package/dist/dashboard/assets/drawer-ZbEZ_84c.js.gz +0 -0
  60. package/dist/dashboard/assets/{entry-DcobaKFu.js → entry-BJfsVMvA.js} +1 -1
  61. package/dist/dashboard/assets/entry-BJfsVMvA.js.gz +0 -0
  62. package/dist/dashboard/assets/{entry-DOddwoc0.js → entry-CWBY5A0f.js} +3 -3
  63. package/dist/dashboard/assets/entry-CWBY5A0f.js.gz +0 -0
  64. package/dist/dashboard/assets/{entry-BRszm4_Z.js → entry-CvC1-Zqx.js} +1 -1
  65. package/dist/dashboard/assets/entry-CvC1-Zqx.js.gz +0 -0
  66. package/dist/dashboard/assets/{github-badge-BuDsrsPn.js → github-badge-BxpznFLt.js} +1 -1
  67. package/dist/dashboard/assets/github-badge-BxpznFLt.js.gz +0 -0
  68. package/dist/dashboard/assets/{index-BeLRhsmt.js → index-CDzP3ovJ.js} +2 -2
  69. package/dist/dashboard/assets/index-CDzP3ovJ.js.gz +0 -0
  70. package/dist/dashboard/assets/{inline-DToMICwl.js → inline-Ae13MQ5w.js} +1 -1
  71. package/dist/dashboard/assets/inline-Ae13MQ5w.js.gz +0 -0
  72. package/dist/dashboard/assets/{input-WlyHWeG2.js → input-D23fo7cp.js} +1 -1
  73. package/dist/dashboard/assets/input-D23fo7cp.js.gz +0 -0
  74. package/dist/dashboard/assets/{layout-DaJ5baJr.js → layout-CSsxjj5P.js} +1 -1
  75. package/dist/dashboard/assets/layout-CSsxjj5P.js.gz +0 -0
  76. package/dist/dashboard/assets/{list-row-B3oSK13z.js → list-row-dmbwZMDY.js} +1 -1
  77. package/dist/dashboard/assets/list-row-dmbwZMDY.js.gz +0 -0
  78. package/dist/dashboard/assets/{live-ta6GMb-8.js → live-BG1oUsZZ.js} +1 -1
  79. package/dist/dashboard/assets/live-BG1oUsZZ.js.gz +0 -0
  80. package/dist/dashboard/assets/minimize-2-D-MN8W8a.js +1 -0
  81. package/dist/dashboard/assets/minimize-2-D-MN8W8a.js.gz +0 -0
  82. package/dist/dashboard/assets/{mutation-g_fduiNw.js → mutation-DSF-e4ho.js} +1 -1
  83. package/dist/dashboard/assets/mutation-DSF-e4ho.js.gz +0 -0
  84. package/dist/dashboard/assets/{numbers-Dulqgys5.js → numbers-Bm_aECY7.js} +1 -1
  85. package/dist/dashboard/assets/{numbers-Dulqgys5.js.gz → numbers-Bm_aECY7.js.gz} +0 -0
  86. package/dist/dashboard/assets/{query-keys-UETqfytp.js → query-keys-CF56nYIV.js} +1 -1
  87. package/dist/dashboard/assets/query-keys-CF56nYIV.js.gz +0 -0
  88. package/dist/dashboard/assets/{routing-BPUo-W8d.js → routing-CeA71n7G.js} +1 -1
  89. package/dist/dashboard/assets/routing-CeA71n7G.js.gz +0 -0
  90. package/dist/dashboard/assets/{select-BfZ3SCTM.js → select-KIT0rC6G.js} +1 -1
  91. package/dist/dashboard/assets/select-KIT0rC6G.js.gz +0 -0
  92. package/dist/dashboard/assets/settings-B_2gkePf.js +1 -0
  93. package/dist/dashboard/assets/settings-B_2gkePf.js.gz +0 -0
  94. package/dist/dashboard/assets/{stack-B1w5BM0u.js → stack-D7WOSzcX.js} +1 -1
  95. package/dist/dashboard/assets/stack-D7WOSzcX.js.gz +0 -0
  96. package/dist/dashboard/assets/{state-DhZBfWgx.js → state-BBxtKZOQ.js} +1 -1
  97. package/dist/dashboard/assets/state-BBxtKZOQ.js.gz +0 -0
  98. package/dist/dashboard/assets/{switch-yD7D99Lg.js → switch-DNS_D5Ea.js} +1 -1
  99. package/dist/dashboard/assets/switch-DNS_D5Ea.js.gz +0 -0
  100. package/dist/dashboard/assets/{useMutation-EDFRkn1i.js → useMutation-BgoxdoMk.js} +1 -1
  101. package/dist/dashboard/assets/useMutation-BgoxdoMk.js.gz +0 -0
  102. package/dist/dashboard/index.html +1 -1
  103. package/dist/dashboard/index.html.gz +0 -0
  104. package/dist/dashboard/precompressed-assets.json +51 -51
  105. package/package.json +96 -96
  106. package/src/boot/app.ts +714 -722
  107. package/src/boot/dependencies.ts +729 -750
  108. package/src/config/resources.ts +70 -137
  109. package/src/config/security.ts +37 -73
  110. package/src/config/spec.ts +358 -378
  111. package/src/console/console-router.ts +225 -233
  112. package/src/console/domain-registration.ts +381 -391
  113. package/src/console/remote-support/contracts.ts +3 -1
  114. package/src/console/remote-support/service.ts +115 -6
  115. package/src/main.ts +167 -169
  116. package/src/observability/metrics.ts +467 -457
  117. package/src/observability/runtime-metrics.ts +269 -254
  118. package/src/protocol/gateway-error.ts +232 -233
  119. package/src/providers/integrations/commandcode/commandcode.ts +134 -104
  120. package/src/providers/integrations/commandcode/discovery.ts +6 -5
  121. package/src/providers/integrations/qoder/qoder.ts +597 -23
  122. package/src/security/abuse.ts +17 -0
  123. package/src/transport/guards/auth-guard.ts +109 -127
  124. package/src/transport/guards/pipeline.ts +220 -226
  125. package/src/transport/guards/request-context.ts +238 -318
  126. package/src/transport/request/inflight.ts +21 -2
  127. package/dist/dashboard/assets/BannedPage-Dskhyp0l.js.gz +0 -0
  128. package/dist/dashboard/assets/CliToolDetailPage--SJhk7qQ.js.gz +0 -0
  129. package/dist/dashboard/assets/CliToolsPage-C203VT-c.js.gz +0 -0
  130. package/dist/dashboard/assets/CombosPage-BcS7kGG1.js.gz +0 -0
  131. package/dist/dashboard/assets/ConsoleLogPage-ChmiSLwO.js.gz +0 -0
  132. package/dist/dashboard/assets/CustomizationPage-uujbBYwC.js.gz +0 -0
  133. package/dist/dashboard/assets/HealthEventsModal-fKAWd-OQ.js.gz +0 -0
  134. package/dist/dashboard/assets/LoginPage-DoOUthbG.js.gz +0 -0
  135. package/dist/dashboard/assets/ModelPicker-CA1kLs45.js.gz +0 -0
  136. package/dist/dashboard/assets/OverviewPage-C5Z-UBz1.js +0 -1
  137. package/dist/dashboard/assets/OverviewPage-C5Z-UBz1.js.gz +0 -0
  138. package/dist/dashboard/assets/ProviderDetailPage-CFu1qGcT.js.gz +0 -0
  139. package/dist/dashboard/assets/ProvidersPage-CnlJZbxF.js.gz +0 -0
  140. package/dist/dashboard/assets/ProxyPage-B9mBTweh.js.gz +0 -0
  141. package/dist/dashboard/assets/QuotaPage-BueSWX5D.js.gz +0 -0
  142. package/dist/dashboard/assets/SettingsPage-B0x00MVY.js +0 -1
  143. package/dist/dashboard/assets/SettingsPage-B0x00MVY.js.gz +0 -0
  144. package/dist/dashboard/assets/SetupPage-DQ9oDRpi.js.gz +0 -0
  145. package/dist/dashboard/assets/SortableList-DrKeMcGN.js.gz +0 -0
  146. package/dist/dashboard/assets/StudioPage-qjuYtccc.js.gz +0 -0
  147. package/dist/dashboard/assets/UsagePage-Be7aNviQ.js +0 -56
  148. package/dist/dashboard/assets/UsagePage-Be7aNviQ.js.gz +0 -0
  149. package/dist/dashboard/assets/api-keys-C23S9sgp.js.gz +0 -0
  150. package/dist/dashboard/assets/app-BUSiYL6F.css +0 -2
  151. package/dist/dashboard/assets/app-BUSiYL6F.css.gz +0 -0
  152. package/dist/dashboard/assets/assert-DU6qrMA_.js.gz +0 -0
  153. package/dist/dashboard/assets/badge-Bkz_-RrM.js.gz +0 -0
  154. package/dist/dashboard/assets/banner-C-7y3VKV.js.gz +0 -0
  155. package/dist/dashboard/assets/card-BOPBuBzD.js.gz +0 -0
  156. package/dist/dashboard/assets/cli-tools-BaFUuFFF.js.gz +0 -0
  157. package/dist/dashboard/assets/clipboard-button-e-Mlk0FH.js.gz +0 -0
  158. package/dist/dashboard/assets/dialog-Iec_ywbD.js.gz +0 -0
  159. package/dist/dashboard/assets/drawer-Bs8MIIHc.js.gz +0 -0
  160. package/dist/dashboard/assets/entry-BRszm4_Z.js.gz +0 -0
  161. package/dist/dashboard/assets/entry-DOddwoc0.js.gz +0 -0
  162. package/dist/dashboard/assets/entry-DcobaKFu.js.gz +0 -0
  163. package/dist/dashboard/assets/github-badge-BuDsrsPn.js.gz +0 -0
  164. package/dist/dashboard/assets/index-BeLRhsmt.js.gz +0 -0
  165. package/dist/dashboard/assets/inline-DToMICwl.js.gz +0 -0
  166. package/dist/dashboard/assets/input-WlyHWeG2.js.gz +0 -0
  167. package/dist/dashboard/assets/layout-DaJ5baJr.js.gz +0 -0
  168. package/dist/dashboard/assets/list-row-B3oSK13z.js.gz +0 -0
  169. package/dist/dashboard/assets/live-ta6GMb-8.js.gz +0 -0
  170. package/dist/dashboard/assets/model-bans-BmYTmbAk.js +0 -1
  171. package/dist/dashboard/assets/model-bans-BmYTmbAk.js.gz +0 -0
  172. package/dist/dashboard/assets/mutation-g_fduiNw.js.gz +0 -0
  173. package/dist/dashboard/assets/query-keys-UETqfytp.js.gz +0 -0
  174. package/dist/dashboard/assets/routing-BPUo-W8d.js.gz +0 -0
  175. package/dist/dashboard/assets/select-BfZ3SCTM.js.gz +0 -0
  176. package/dist/dashboard/assets/settings--3hxSyry.js +0 -1
  177. package/dist/dashboard/assets/settings--3hxSyry.js.gz +0 -0
  178. package/dist/dashboard/assets/stack-B1w5BM0u.js.gz +0 -0
  179. package/dist/dashboard/assets/state-DhZBfWgx.js.gz +0 -0
  180. package/dist/dashboard/assets/switch-yD7D99Lg.js.gz +0 -0
  181. package/dist/dashboard/assets/useMutation-EDFRkn1i.js.gz +0 -0
  182. package/src/console/admin/model-abuse/contracts.ts +0 -38
  183. package/src/console/admin/model-abuse/routes.ts +0 -97
  184. package/src/security/model-abuse.ts +0 -704
  185. package/src/transport/guards/concurrency-guard.ts +0 -125
  186. /package/dist/dashboard/assets/{app-MdVfwraB.js → app-CaXZ9SCn.js} +0 -0
  187. /package/dist/dashboard/assets/{app-MdVfwraB.js.gz → app-CaXZ9SCn.js.gz} +0 -0
@@ -1,233 +1,232 @@
1
- // Typed gateway failures: the stable public error codes, the error class, and
2
- // the sanitizers that keep upstream payloads out of public envelopes.
3
-
4
- /** Stable typed error codes returned at gateway boundaries. */
5
- export type GatewayErrorCode =
6
- | "capability_unsupported"
7
- | "ambiguous_model"
8
- | "model_not_found"
9
- | "upstream_not_found"
10
- | "context_length_exceeded"
11
- | "request_too_large"
12
- | "accounts_unavailable"
13
- | "accounts_rate_limited"
14
- | "capacity_exhausted"
15
- | "provider_capacity_exhausted"
16
- | "upstream_queued"
17
- | "proxy_pool_capacity_exceeded"
18
- | "proxy_pool_provider_capacity_exceeded"
19
- | "proxy_pool_blocked"
20
- | "proxy_pool_cooldown"
21
- | "proxy_pool_unavailable"
22
- | "proxy_pool_unhealthy"
23
- | "invalid_pool_limits"
24
- | "admission_unavailable"
25
- | "slug_reserved"
26
- | "quota_exceeded"
27
- | "authentication_failed"
28
- | "policy_rejected"
29
- | "invalid_request"
30
- | "not_found"
31
- | "link_not_found"
32
- | "link_expired"
33
- | "link_revoked"
34
- | "link_disabled"
35
- | "invalid_sequence"
36
- | "invalid_lifecycle"
37
- | "unsupported_field"
38
- | "unsupported_media_type"
39
- | "upstream_conflict"
40
- | "upstream_unprocessable"
41
- | "internal_error"
42
- | "transport_unavailable"
43
- | "tunnel_setup_failed"
44
- | "platform_unavailable"
45
- | "tenant_capacity_exhausted"
46
- | "tls_rejected"
47
- | "proxy_auth_required"
48
- | "deadline_exceeded"
49
- | "transport_closed"
50
- | "max_connections_exceeded"
51
- | "overloaded"
52
- | "proxy_unreachable"
53
- | "tool_call_loop_detected"
54
- | "client_router_denied"
55
- | "model_abuse_banned"
56
- | "shutting_down"
57
- | "restart_for_update";
58
-
59
- /** Identifies which boundary produced a safe public error. */
60
- export type GatewayErrorOrigin = "cartethyia" | "upstream" | "network";
61
-
62
- /**
63
- * Error with a client-safe stable code, origin, and sanitized metadata.
64
- */
65
- export class GatewayError extends Error {
66
- readonly code: GatewayErrorCode;
67
- readonly status: number;
68
- readonly origin: GatewayErrorOrigin;
69
- readonly details: Readonly<Record<string, unknown>>;
70
-
71
- constructor(
72
- code: GatewayErrorCode,
73
- status: number,
74
- message: string,
75
- details: Readonly<Record<string, unknown>> = {},
76
- origin: GatewayErrorOrigin = "cartethyia",
77
- ) {
78
- super(message);
79
- this.name = "GatewayError";
80
- this.code = code;
81
- this.status = status;
82
- this.origin = origin;
83
- this.details = details;
84
- }
85
- }
86
-
87
- /**
88
- * Legacy origin brand prefixes. Older builds stamped these into the public
89
- * `message`; strip them so a value that crossed two shapers is not doubled
90
- * and so clients never see product branding in the error text.
91
- *
92
- * Blame lives in the structured `origin` field (`cartethyia` | `upstream` |
93
- * `network`). The public message is always `code: explanatory` for every
94
- * origin — gateway and upstream look the same on the wire.
95
- */
96
- const LEGACY_ORIGIN_LABELS: readonly string[] = [
97
- "Cartethyia Error:",
98
- "Upstream Error:",
99
- "Network Error:",
100
- ];
101
-
102
- function stripLegacyOriginLabel(message: string): string {
103
- const trimmed = message.trim();
104
- for (const prefix of LEGACY_ORIGIN_LABELS) {
105
- if (trimmed.startsWith(prefix)) return trimmed.slice(prefix.length).trimStart();
106
- }
107
- return trimmed;
108
- }
109
-
110
- /**
111
- * Public client-facing error text: a plain sentence, with no machine code
112
- * stamped into it.
113
- *
114
- * The code is not lost — it travels in the envelope's `error.code` field, and
115
- * the HTTP status carries the class. Stamping it into the text
116
- * (`accounts_unavailable: Model 'x' has no available account (52 candidate(s)
117
- * unusable: credit_floor_reached, model_cooldown)`) would put internal enum
118
- * names in front of the operator's sentence and make the message unreadable
119
- * while adding nothing the field does not already carry.
120
- *
121
- * Stripping is idempotent, and that is the point: a message that already
122
- * carries a `code:` prefix — stamped by an older builder, or by a code path
123
- * that formats before it reaches here — loses it once instead of being
124
- * doubled. Origin is never written into the text either; clients that need the
125
- * layer read `error.origin`.
126
- */
127
- export function formatPublicErrorMessage(code: string, message: string): string {
128
- const strippedOrigin = stripLegacyOriginLabel(message);
129
- // A leading `<code>:` is the old stamping; drop it so the sentence stands on
130
- // its own. Matched against the code this call is formatting, so a message
131
- // that legitimately begins with some other word is left alone.
132
- const prefix = `${code}:`;
133
- const explanatory = strippedOrigin.startsWith(prefix)
134
- ? strippedOrigin.slice(prefix.length).trimStart()
135
- : strippedOrigin;
136
- if (explanatory.length === 0) return code;
137
- return explanatory;
138
- }
139
-
140
- /** Formats a gateway error for public/internal clients without mutating it. */
141
- export function explainGatewayError(error: GatewayError): string {
142
- return formatPublicErrorMessage(error.code, error.message);
143
- }
144
-
145
- const PUBLIC_ERROR_DETAIL_KEYS = new Set([
146
- "accountId",
147
- "accountScope",
148
- "available",
149
- "capacity",
150
- "configuredPools",
151
- "cooldownReason",
152
- "gateway_hint",
153
- "currentInflight",
154
- "credentialEvidence",
155
- "model",
156
- "poolId",
157
- "pools",
158
- "providerCode",
159
- "providerId",
160
- "providerStatus",
161
- "providerScope",
162
- "raw",
163
- "rateLimitScope",
164
- "reason",
165
- "reasons",
166
- "candidate_count",
167
- "requestId",
168
- "retryAfterMs",
169
- "retryAt",
170
- "safeMessage",
171
- "scope",
172
- "status",
173
- "upstreamRequestId",
174
- "upstreamStatus",
175
- "maxInflight",
176
- "weight",
177
- ]);
178
- function sanitizePublicDetail(value: unknown, depth = 0): unknown {
179
- if (typeof value === "string") return value.slice(0, 500);
180
- if (typeof value !== "object" || value === null) return value;
181
- if (depth >= 3) return "[redacted]";
182
- if (Array.isArray(value))
183
- return value.slice(0, 32).map((entry) => sanitizePublicDetail(entry, depth + 1));
184
- return Object.fromEntries(
185
- Object.entries(value).map(([key, entry]) => [
186
- key,
187
- sanitizePublicDetail(entry, depth + 1),
188
- ]),
189
- );
190
- }
191
-
192
- /** Returns bounded allowlisted details suitable for public error envelopes. */
193
- export function publicGatewayErrorDetails(
194
- error: GatewayError,
195
- ): Readonly<Record<string, unknown>> {
196
- return Object.fromEntries(
197
- Object.entries(error.details)
198
- .filter(([key]) => PUBLIC_ERROR_DETAIL_KEYS.has(key))
199
- .map(([key, value]) => [key, sanitizePublicDetail(value)]),
200
- );
201
- }
202
-
203
- /** Serializes a GatewayError with the same public shape as transport errors. */
204
- export function publicGatewayErrorBody(error: GatewayError): {
205
- readonly error: {
206
- readonly origin: GatewayErrorOrigin;
207
- readonly code: GatewayErrorCode;
208
- readonly message: string;
209
- readonly details: Readonly<Record<string, unknown>>;
210
- };
211
- } {
212
- return {
213
- error: {
214
- origin: error.origin,
215
- code: error.code,
216
- message: explainGatewayError(error),
217
- details: publicGatewayErrorDetails(error),
218
- },
219
- };
220
- }
221
-
222
- /** Creates the typed rejection required when a semantic feature is unavailable. */
223
- export function capabilityUnsupported(
224
- capability: string,
225
- details: Readonly<Record<string, unknown>> = {},
226
- ): GatewayError {
227
- return new GatewayError(
228
- "capability_unsupported",
229
- 400,
230
- `The selected route does not support capability: ${capability}`,
231
- { capability, ...details },
232
- );
233
- }
1
+ // Typed gateway failures: the stable public error codes, the error class, and
2
+ // the sanitizers that keep upstream payloads out of public envelopes.
3
+
4
+ /** Stable typed error codes returned at gateway boundaries. */
5
+ export type GatewayErrorCode =
6
+ | "capability_unsupported"
7
+ | "ambiguous_model"
8
+ | "model_not_found"
9
+ | "upstream_not_found"
10
+ | "context_length_exceeded"
11
+ | "request_too_large"
12
+ | "accounts_unavailable"
13
+ | "accounts_rate_limited"
14
+ | "capacity_exhausted"
15
+ | "provider_capacity_exhausted"
16
+ | "upstream_queued"
17
+ | "proxy_pool_capacity_exceeded"
18
+ | "proxy_pool_provider_capacity_exceeded"
19
+ | "proxy_pool_blocked"
20
+ | "proxy_pool_cooldown"
21
+ | "proxy_pool_unavailable"
22
+ | "proxy_pool_unhealthy"
23
+ | "invalid_pool_limits"
24
+ | "admission_unavailable"
25
+ | "slug_reserved"
26
+ | "quota_exceeded"
27
+ | "authentication_failed"
28
+ | "policy_rejected"
29
+ | "invalid_request"
30
+ | "not_found"
31
+ | "link_not_found"
32
+ | "link_expired"
33
+ | "link_revoked"
34
+ | "link_disabled"
35
+ | "invalid_sequence"
36
+ | "invalid_lifecycle"
37
+ | "unsupported_field"
38
+ | "unsupported_media_type"
39
+ | "upstream_conflict"
40
+ | "upstream_unprocessable"
41
+ | "internal_error"
42
+ | "transport_unavailable"
43
+ | "tunnel_setup_failed"
44
+ | "platform_unavailable"
45
+ | "tenant_capacity_exhausted"
46
+ | "tls_rejected"
47
+ | "proxy_auth_required"
48
+ | "deadline_exceeded"
49
+ | "transport_closed"
50
+ | "max_connections_exceeded"
51
+ | "overloaded"
52
+ | "proxy_unreachable"
53
+ | "tool_call_loop_detected"
54
+ | "client_router_denied"
55
+ | "shutting_down"
56
+ | "restart_for_update";
57
+
58
+ /** Identifies which boundary produced a safe public error. */
59
+ export type GatewayErrorOrigin = "cartethyia" | "upstream" | "network";
60
+
61
+ /**
62
+ * Error with a client-safe stable code, origin, and sanitized metadata.
63
+ */
64
+ export class GatewayError extends Error {
65
+ readonly code: GatewayErrorCode;
66
+ readonly status: number;
67
+ readonly origin: GatewayErrorOrigin;
68
+ readonly details: Readonly<Record<string, unknown>>;
69
+
70
+ constructor(
71
+ code: GatewayErrorCode,
72
+ status: number,
73
+ message: string,
74
+ details: Readonly<Record<string, unknown>> = {},
75
+ origin: GatewayErrorOrigin = "cartethyia",
76
+ ) {
77
+ super(message);
78
+ this.name = "GatewayError";
79
+ this.code = code;
80
+ this.status = status;
81
+ this.origin = origin;
82
+ this.details = details;
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Legacy origin brand prefixes. Older builds stamped these into the public
88
+ * `message`; strip them so a value that crossed two shapers is not doubled
89
+ * and so clients never see product branding in the error text.
90
+ *
91
+ * Blame lives in the structured `origin` field (`cartethyia` | `upstream` |
92
+ * `network`). The public message is always `code: explanatory` for every
93
+ * origin — gateway and upstream look the same on the wire.
94
+ */
95
+ const LEGACY_ORIGIN_LABELS: readonly string[] = [
96
+ "Cartethyia Error:",
97
+ "Upstream Error:",
98
+ "Network Error:",
99
+ ];
100
+
101
+ function stripLegacyOriginLabel(message: string): string {
102
+ const trimmed = message.trim();
103
+ for (const prefix of LEGACY_ORIGIN_LABELS) {
104
+ if (trimmed.startsWith(prefix)) return trimmed.slice(prefix.length).trimStart();
105
+ }
106
+ return trimmed;
107
+ }
108
+
109
+ /**
110
+ * Public client-facing error text: a plain sentence, with no machine code
111
+ * stamped into it.
112
+ *
113
+ * The code is not lost — it travels in the envelope's `error.code` field, and
114
+ * the HTTP status carries the class. Stamping it into the text
115
+ * (`accounts_unavailable: Model 'x' has no available account (52 candidate(s)
116
+ * unusable: credit_floor_reached, model_cooldown)`) would put internal enum
117
+ * names in front of the operator's sentence and make the message unreadable
118
+ * while adding nothing the field does not already carry.
119
+ *
120
+ * Stripping is idempotent, and that is the point: a message that already
121
+ * carries a `code:` prefix — stamped by an older builder, or by a code path
122
+ * that formats before it reaches here — loses it once instead of being
123
+ * doubled. Origin is never written into the text either; clients that need the
124
+ * layer read `error.origin`.
125
+ */
126
+ export function formatPublicErrorMessage(code: string, message: string): string {
127
+ const strippedOrigin = stripLegacyOriginLabel(message);
128
+ // A leading `<code>:` is the old stamping; drop it so the sentence stands on
129
+ // its own. Matched against the code this call is formatting, so a message
130
+ // that legitimately begins with some other word is left alone.
131
+ const prefix = `${code}:`;
132
+ const explanatory = strippedOrigin.startsWith(prefix)
133
+ ? strippedOrigin.slice(prefix.length).trimStart()
134
+ : strippedOrigin;
135
+ if (explanatory.length === 0) return code;
136
+ return explanatory;
137
+ }
138
+
139
+ /** Formats a gateway error for public/internal clients without mutating it. */
140
+ export function explainGatewayError(error: GatewayError): string {
141
+ return formatPublicErrorMessage(error.code, error.message);
142
+ }
143
+
144
+ const PUBLIC_ERROR_DETAIL_KEYS = new Set([
145
+ "accountId",
146
+ "accountScope",
147
+ "available",
148
+ "capacity",
149
+ "configuredPools",
150
+ "cooldownReason",
151
+ "gateway_hint",
152
+ "currentInflight",
153
+ "credentialEvidence",
154
+ "model",
155
+ "poolId",
156
+ "pools",
157
+ "providerCode",
158
+ "providerId",
159
+ "providerStatus",
160
+ "providerScope",
161
+ "raw",
162
+ "rateLimitScope",
163
+ "reason",
164
+ "reasons",
165
+ "candidate_count",
166
+ "requestId",
167
+ "retryAfterMs",
168
+ "retryAt",
169
+ "safeMessage",
170
+ "scope",
171
+ "status",
172
+ "upstreamRequestId",
173
+ "upstreamStatus",
174
+ "maxInflight",
175
+ "weight",
176
+ ]);
177
+ function sanitizePublicDetail(value: unknown, depth = 0): unknown {
178
+ if (typeof value === "string") return value.slice(0, 500);
179
+ if (typeof value !== "object" || value === null) return value;
180
+ if (depth >= 3) return "[redacted]";
181
+ if (Array.isArray(value))
182
+ return value.slice(0, 32).map((entry) => sanitizePublicDetail(entry, depth + 1));
183
+ return Object.fromEntries(
184
+ Object.entries(value).map(([key, entry]) => [
185
+ key,
186
+ sanitizePublicDetail(entry, depth + 1),
187
+ ]),
188
+ );
189
+ }
190
+
191
+ /** Returns bounded allowlisted details suitable for public error envelopes. */
192
+ export function publicGatewayErrorDetails(
193
+ error: GatewayError,
194
+ ): Readonly<Record<string, unknown>> {
195
+ return Object.fromEntries(
196
+ Object.entries(error.details)
197
+ .filter(([key]) => PUBLIC_ERROR_DETAIL_KEYS.has(key))
198
+ .map(([key, value]) => [key, sanitizePublicDetail(value)]),
199
+ );
200
+ }
201
+
202
+ /** Serializes a GatewayError with the same public shape as transport errors. */
203
+ export function publicGatewayErrorBody(error: GatewayError): {
204
+ readonly error: {
205
+ readonly origin: GatewayErrorOrigin;
206
+ readonly code: GatewayErrorCode;
207
+ readonly message: string;
208
+ readonly details: Readonly<Record<string, unknown>>;
209
+ };
210
+ } {
211
+ return {
212
+ error: {
213
+ origin: error.origin,
214
+ code: error.code,
215
+ message: explainGatewayError(error),
216
+ details: publicGatewayErrorDetails(error),
217
+ },
218
+ };
219
+ }
220
+
221
+ /** Creates the typed rejection required when a semantic feature is unavailable. */
222
+ export function capabilityUnsupported(
223
+ capability: string,
224
+ details: Readonly<Record<string, unknown>> = {},
225
+ ): GatewayError {
226
+ return new GatewayError(
227
+ "capability_unsupported",
228
+ 400,
229
+ `The selected route does not support capability: ${capability}`,
230
+ { capability, ...details },
231
+ );
232
+ }