aigetwey 1.0.1

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 (216) hide show
  1. package/CHANGELOG.md +84 -0
  2. package/LICENSE +21 -0
  3. package/README.md +302 -0
  4. package/assets/logo.svg +8 -0
  5. package/assets/screenshot.png +0 -0
  6. package/assets/wordmark.svg +9 -0
  7. package/config.example.yaml +56 -0
  8. package/dashboard/.env.example +12 -0
  9. package/dashboard/next-env.d.ts +6 -0
  10. package/dashboard/next.config.ts +12 -0
  11. package/dashboard/package-lock.json +1771 -0
  12. package/dashboard/package.json +29 -0
  13. package/dashboard/postcss.config.mjs +5 -0
  14. package/dashboard/src/app/(console)/combos/page.tsx +10 -0
  15. package/dashboard/src/app/(console)/config/page.tsx +5 -0
  16. package/dashboard/src/app/(console)/console/page.tsx +92 -0
  17. package/dashboard/src/app/(console)/endpoint/page.tsx +5 -0
  18. package/dashboard/src/app/(console)/layout.tsx +17 -0
  19. package/dashboard/src/app/(console)/page.tsx +8 -0
  20. package/dashboard/src/app/(console)/providers/[id]/page.tsx +6 -0
  21. package/dashboard/src/app/(console)/providers/page.tsx +5 -0
  22. package/dashboard/src/app/(console)/quota/page.tsx +5 -0
  23. package/dashboard/src/app/(console)/tools/[id]/page.tsx +6 -0
  24. package/dashboard/src/app/(console)/tools/page.tsx +5 -0
  25. package/dashboard/src/app/(console)/usage/page.tsx +24 -0
  26. package/dashboard/src/app/api/cli-detect/[tool]/route.ts +253 -0
  27. package/dashboard/src/app/api/gw/[...path]/route.ts +89 -0
  28. package/dashboard/src/app/api/login/route.ts +30 -0
  29. package/dashboard/src/app/api/logout/route.ts +9 -0
  30. package/dashboard/src/app/api/password/route.ts +34 -0
  31. package/dashboard/src/app/globals.css +340 -0
  32. package/dashboard/src/app/icon.svg +8 -0
  33. package/dashboard/src/app/layout.tsx +28 -0
  34. package/dashboard/src/app/login/page.tsx +60 -0
  35. package/dashboard/src/components/AreaChart.tsx +115 -0
  36. package/dashboard/src/components/Badge.tsx +32 -0
  37. package/dashboard/src/components/Button.tsx +60 -0
  38. package/dashboard/src/components/CapacityBadges.tsx +40 -0
  39. package/dashboard/src/components/Checkbox.tsx +40 -0
  40. package/dashboard/src/components/CliToolConfig.tsx +63 -0
  41. package/dashboard/src/components/ConfigEditor.tsx +199 -0
  42. package/dashboard/src/components/ConfirmModal.tsx +36 -0
  43. package/dashboard/src/components/CooldownTimer.tsx +42 -0
  44. package/dashboard/src/components/EndpointView.tsx +439 -0
  45. package/dashboard/src/components/Icon.tsx +25 -0
  46. package/dashboard/src/components/KeyReveal.tsx +78 -0
  47. package/dashboard/src/components/Lamp.tsx +8 -0
  48. package/dashboard/src/components/LogTable.tsx +223 -0
  49. package/dashboard/src/components/LogoutButton.tsx +20 -0
  50. package/dashboard/src/components/ModelPicker.tsx +121 -0
  51. package/dashboard/src/components/ModelSelectModal.tsx +126 -0
  52. package/dashboard/src/components/PasswordEditor.tsx +86 -0
  53. package/dashboard/src/components/PricingEditor.tsx +171 -0
  54. package/dashboard/src/components/ProviderDetail.tsx +566 -0
  55. package/dashboard/src/components/ProviderManager.tsx +311 -0
  56. package/dashboard/src/components/QuotaView.tsx +78 -0
  57. package/dashboard/src/components/Rail.tsx +82 -0
  58. package/dashboard/src/components/RichCard.tsx +46 -0
  59. package/dashboard/src/components/RoutingView.tsx +329 -0
  60. package/dashboard/src/components/ThemeProvider.tsx +36 -0
  61. package/dashboard/src/components/ToastProvider.tsx +58 -0
  62. package/dashboard/src/components/ToolDetail.tsx +475 -0
  63. package/dashboard/src/components/TopBar.tsx +128 -0
  64. package/dashboard/src/components/UsageView.tsx +151 -0
  65. package/dashboard/src/components/ui.tsx +54 -0
  66. package/dashboard/src/lib/capabilities.ts +318 -0
  67. package/dashboard/src/lib/cliTools.ts +120 -0
  68. package/dashboard/src/lib/client.ts +190 -0
  69. package/dashboard/src/lib/gateway.ts +269 -0
  70. package/dashboard/src/lib/session.ts +71 -0
  71. package/dashboard/src/middleware.ts +37 -0
  72. package/dashboard/tsconfig.json +21 -0
  73. package/dist/adapters/anthropic.js +289 -0
  74. package/dist/adapters/anthropic.js.map +1 -0
  75. package/dist/adapters/gemini.js +268 -0
  76. package/dist/adapters/gemini.js.map +1 -0
  77. package/dist/adapters/index.js +8 -0
  78. package/dist/adapters/index.js.map +1 -0
  79. package/dist/adapters/openai.js +13 -0
  80. package/dist/adapters/openai.js.map +1 -0
  81. package/dist/cli/tray/autostart.js +152 -0
  82. package/dist/cli/tray/autostart.js.map +1 -0
  83. package/dist/cli/tray/icon.js +4 -0
  84. package/dist/cli/tray/icon.js.map +1 -0
  85. package/dist/cli/tray/tray.js +141 -0
  86. package/dist/cli/tray/tray.js.map +1 -0
  87. package/dist/cli/tray/trayRuntime.js +91 -0
  88. package/dist/cli/tray/trayRuntime.js.map +1 -0
  89. package/dist/cli.js +361 -0
  90. package/dist/cli.js.map +1 -0
  91. package/dist/config.js +728 -0
  92. package/dist/config.js.map +1 -0
  93. package/dist/core/authStore.js +78 -0
  94. package/dist/core/authStore.js.map +1 -0
  95. package/dist/core/canonical.js +9 -0
  96. package/dist/core/canonical.js.map +1 -0
  97. package/dist/core/console-buffer.js +25 -0
  98. package/dist/core/console-buffer.js.map +1 -0
  99. package/dist/core/fallback.js +62 -0
  100. package/dist/core/fallback.js.map +1 -0
  101. package/dist/core/handler.js +174 -0
  102. package/dist/core/handler.js.map +1 -0
  103. package/dist/core/keypool.js +105 -0
  104. package/dist/core/keypool.js.map +1 -0
  105. package/dist/core/quota.js +165 -0
  106. package/dist/core/quota.js.map +1 -0
  107. package/dist/core/state.js +52 -0
  108. package/dist/core/state.js.map +1 -0
  109. package/dist/db.js +193 -0
  110. package/dist/db.js.map +1 -0
  111. package/dist/headroom/compress.js +44 -0
  112. package/dist/headroom/compress.js.map +1 -0
  113. package/dist/headroom/detect.js +108 -0
  114. package/dist/headroom/detect.js.map +1 -0
  115. package/dist/headroom/process.js +158 -0
  116. package/dist/headroom/process.js.map +1 -0
  117. package/dist/inject/caveman.js +30 -0
  118. package/dist/inject/caveman.js.map +1 -0
  119. package/dist/inject/index.js +24 -0
  120. package/dist/inject/index.js.map +1 -0
  121. package/dist/inject/ponytail.js +19 -0
  122. package/dist/inject/ponytail.js.map +1 -0
  123. package/dist/middleware/auth.js +66 -0
  124. package/dist/middleware/auth.js.map +1 -0
  125. package/dist/providers/capabilities.js +246 -0
  126. package/dist/providers/capabilities.js.map +1 -0
  127. package/dist/providers/free.js +43 -0
  128. package/dist/providers/free.js.map +1 -0
  129. package/dist/providers/pricing.js +224 -0
  130. package/dist/providers/pricing.js.map +1 -0
  131. package/dist/providers/vertex.js +97 -0
  132. package/dist/providers/vertex.js.map +1 -0
  133. package/dist/routes/admin.js +622 -0
  134. package/dist/routes/admin.js.map +1 -0
  135. package/dist/routes/health.js +4 -0
  136. package/dist/routes/health.js.map +1 -0
  137. package/dist/routes/index.js +12 -0
  138. package/dist/routes/index.js.map +1 -0
  139. package/dist/routes/v1.js +75 -0
  140. package/dist/routes/v1.js.map +1 -0
  141. package/dist/rtk/detect.js +50 -0
  142. package/dist/rtk/detect.js.map +1 -0
  143. package/dist/rtk/filters.js +85 -0
  144. package/dist/rtk/filters.js.map +1 -0
  145. package/dist/rtk/index.js +39 -0
  146. package/dist/rtk/index.js.map +1 -0
  147. package/dist/server.js +100 -0
  148. package/dist/server.js.map +1 -0
  149. package/dist/stream/anthropic-stream.js +239 -0
  150. package/dist/stream/anthropic-stream.js.map +1 -0
  151. package/dist/stream/chunk.js +7 -0
  152. package/dist/stream/chunk.js.map +1 -0
  153. package/dist/stream/gemini-stream.js +135 -0
  154. package/dist/stream/gemini-stream.js.map +1 -0
  155. package/dist/stream/index.js +12 -0
  156. package/dist/stream/index.js.map +1 -0
  157. package/dist/stream/openai-stream.js +34 -0
  158. package/dist/stream/openai-stream.js.map +1 -0
  159. package/dist/stream/sse.js +64 -0
  160. package/dist/stream/sse.js.map +1 -0
  161. package/dist/translator/thinking.js +70 -0
  162. package/dist/translator/thinking.js.map +1 -0
  163. package/dist/translator/thinkingUnified.js +322 -0
  164. package/dist/translator/thinkingUnified.js.map +1 -0
  165. package/dist/upstream/client.js +120 -0
  166. package/dist/upstream/client.js.map +1 -0
  167. package/package.json +76 -0
  168. package/run.sh +27 -0
  169. package/src/adapters/anthropic.ts +377 -0
  170. package/src/adapters/gemini.ts +341 -0
  171. package/src/adapters/index.ts +17 -0
  172. package/src/adapters/openai.ts +22 -0
  173. package/src/cli/tray/autostart.ts +133 -0
  174. package/src/cli/tray/icon.ts +4 -0
  175. package/src/cli/tray/tray.ts +156 -0
  176. package/src/cli/tray/trayRuntime.ts +90 -0
  177. package/src/cli.ts +379 -0
  178. package/src/config.ts +777 -0
  179. package/src/core/authStore.ts +86 -0
  180. package/src/core/canonical.ts +93 -0
  181. package/src/core/console-buffer.ts +39 -0
  182. package/src/core/fallback.ts +116 -0
  183. package/src/core/handler.ts +236 -0
  184. package/src/core/keypool.ts +152 -0
  185. package/src/core/quota.ts +214 -0
  186. package/src/core/state.ts +65 -0
  187. package/src/db.ts +280 -0
  188. package/src/headroom/compress.ts +78 -0
  189. package/src/headroom/detect.ts +119 -0
  190. package/src/headroom/process.ts +166 -0
  191. package/src/inject/caveman.ts +35 -0
  192. package/src/inject/index.ts +46 -0
  193. package/src/inject/ponytail.ts +31 -0
  194. package/src/middleware/auth.ts +76 -0
  195. package/src/providers/capabilities.ts +297 -0
  196. package/src/providers/free.ts +53 -0
  197. package/src/providers/pricing.ts +261 -0
  198. package/src/providers/vertex.ts +117 -0
  199. package/src/routes/admin.ts +716 -0
  200. package/src/routes/health.ts +5 -0
  201. package/src/routes/index.ts +24 -0
  202. package/src/routes/v1.ts +87 -0
  203. package/src/rtk/detect.ts +55 -0
  204. package/src/rtk/filters.ts +94 -0
  205. package/src/rtk/index.ts +58 -0
  206. package/src/server.ts +108 -0
  207. package/src/stream/anthropic-stream.ts +310 -0
  208. package/src/stream/chunk.ts +46 -0
  209. package/src/stream/gemini-stream.ts +158 -0
  210. package/src/stream/index.ts +23 -0
  211. package/src/stream/openai-stream.ts +41 -0
  212. package/src/stream/sse.ts +72 -0
  213. package/src/translator/thinking.ts +64 -0
  214. package/src/translator/thinkingUnified.ts +319 -0
  215. package/src/upstream/client.ts +155 -0
  216. package/tsconfig.json +20 -0
@@ -0,0 +1,165 @@
1
+ const HOUR_MS = 3600_000;
2
+ const DAY_MS = 24 * HOUR_MS;
3
+ const WEEKDAYS = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"];
4
+ // ---- timezone-aware calendar math -----------------------------------------
5
+ /** Wall-clock offset (ms) of `tz` at instant `date`: tzWallAsUTC - actualUTC. */
6
+ function tzOffsetMs(date, tz) {
7
+ const dtf = new Intl.DateTimeFormat("en-US", {
8
+ timeZone: tz,
9
+ hourCycle: "h23",
10
+ year: "numeric",
11
+ month: "2-digit",
12
+ day: "2-digit",
13
+ hour: "2-digit",
14
+ minute: "2-digit",
15
+ second: "2-digit",
16
+ });
17
+ const parts = Object.fromEntries(dtf.formatToParts(date).map((p) => [p.type, p.value]));
18
+ const asUTC = Date.UTC(Number(parts.year), Number(parts.month) - 1, Number(parts.day), Number(parts.hour), Number(parts.minute), Number(parts.second));
19
+ return asUTC - date.getTime();
20
+ }
21
+ /** Convert a desired wall-clock time in `tz` to an epoch ms. DST-corrected once. */
22
+ function zonedWallToEpoch(y, mo, d, h, mi, tz) {
23
+ const guessUTC = Date.UTC(y, mo, d, h, mi);
24
+ const offset = tzOffsetMs(new Date(guessUTC), tz);
25
+ let epoch = guessUTC - offset;
26
+ // re-check once: the offset can differ across a DST boundary
27
+ const offset2 = tzOffsetMs(new Date(epoch), tz);
28
+ if (offset2 !== offset)
29
+ epoch = guessUTC - offset2;
30
+ return epoch;
31
+ }
32
+ /** Wall-clock parts of `nowMs` in `tz`. */
33
+ function zonedParts(nowMs, tz) {
34
+ const dtf = new Intl.DateTimeFormat("en-US", {
35
+ timeZone: tz,
36
+ hourCycle: "h23",
37
+ weekday: "long",
38
+ year: "numeric",
39
+ month: "2-digit",
40
+ day: "2-digit",
41
+ hour: "2-digit",
42
+ minute: "2-digit",
43
+ });
44
+ const p = Object.fromEntries(dtf.formatToParts(nowMs).map((x) => [x.type, x.value]));
45
+ return {
46
+ year: Number(p.year),
47
+ month: Number(p.month) - 1,
48
+ day: Number(p.day),
49
+ hour: Number(p.hour),
50
+ minute: Number(p.minute),
51
+ weekday: String(p.weekday).toLowerCase(),
52
+ };
53
+ }
54
+ function parseHHMM(reset_at) {
55
+ const m = /^(\d{1,2}):(\d{2})$/.exec(reset_at ?? "");
56
+ if (!m)
57
+ return { h: 0, m: 0 };
58
+ return { h: Math.min(23, Number(m[1])), m: Math.min(59, Number(m[2])) };
59
+ }
60
+ /**
61
+ * Next reset instant (epoch ms) strictly after `now` for a quota schedule.
62
+ * - 5h: rolling — windowStart + 5h.
63
+ * - daily: next `reset_at` (HH:MM, default 00:00) wall-clock in tz.
64
+ * - weekly: next `reset_at` weekday (default monday) at 00:00 in tz.
65
+ * - monthly: next 1st of month at 00:00 in tz.
66
+ */
67
+ export function nextResetAt(quota, windowStart, now) {
68
+ const tz = quota.timezone || "UTC";
69
+ if (quota.window === "5h")
70
+ return windowStart + 5 * HOUR_MS;
71
+ const p = zonedParts(now, tz);
72
+ if (quota.window === "daily") {
73
+ const { h, m } = parseHHMM(quota.reset_at);
74
+ let candidate = zonedWallToEpoch(p.year, p.month, p.day, h, m, tz);
75
+ if (candidate <= now)
76
+ candidate = zonedWallToEpoch(p.year, p.month, p.day + 1, h, m, tz);
77
+ return candidate;
78
+ }
79
+ if (quota.window === "weekly") {
80
+ const target = WEEKDAYS.indexOf((quota.reset_at ?? "monday").toLowerCase());
81
+ const targetIdx = target === -1 ? 1 : target;
82
+ const curIdx = WEEKDAYS.indexOf(p.weekday);
83
+ let daysAhead = (targetIdx - curIdx + 7) % 7;
84
+ let candidate = zonedWallToEpoch(p.year, p.month, p.day + daysAhead, 0, 0, tz);
85
+ if (candidate <= now)
86
+ candidate = zonedWallToEpoch(p.year, p.month, p.day + daysAhead + 7, 0, 0, tz);
87
+ return candidate;
88
+ }
89
+ // monthly: first of next month at 00:00
90
+ return zonedWallToEpoch(p.year, p.month + 1, 1, 0, 0, tz);
91
+ }
92
+ export class QuotaTracker {
93
+ now;
94
+ store;
95
+ states = new Map();
96
+ constructor(now = Date.now, store) {
97
+ this.now = now;
98
+ this.store = store;
99
+ if (store) {
100
+ for (const row of store.load()) {
101
+ this.states.set(row.provider_id, { windowStart: row.window_start, consumed: row.consumed });
102
+ }
103
+ }
104
+ }
105
+ /**
106
+ * Return the live state for a provider, rolling the window over (resetting
107
+ * consumed to 0) if `now` has crossed the scheduled reset boundary.
108
+ */
109
+ current(provider) {
110
+ if (!provider.quota)
111
+ return null;
112
+ const t = this.now();
113
+ const state = this.states.get(provider.id) ?? { windowStart: t, consumed: 0 };
114
+ if (!this.states.has(provider.id))
115
+ this.states.set(provider.id, state);
116
+ // boundary is the first reset AFTER this window opened — computed from
117
+ // windowStart, not `now`. Computing it from `now` would always return the
118
+ // NEXT future boundary and so never detect that we've crossed one.
119
+ const reset = nextResetAt(provider.quota, state.windowStart, state.windowStart);
120
+ if (t >= reset) {
121
+ state.windowStart = t;
122
+ state.consumed = 0;
123
+ this.store?.save(provider.id, state.windowStart, state.consumed);
124
+ }
125
+ return state;
126
+ }
127
+ /** Add consumed tokens for a provider (no-op if it has no quota config). */
128
+ consume(provider, tokens) {
129
+ const state = this.current(provider);
130
+ if (!state)
131
+ return;
132
+ state.consumed += Math.max(0, tokens);
133
+ this.store?.save(provider.id, state.windowStart, state.consumed);
134
+ }
135
+ /** True when a token limit is set AND it's been reached in the current window. */
136
+ isExhausted(provider) {
137
+ const state = this.current(provider);
138
+ if (!state || !provider.quota?.limit_tokens)
139
+ return false;
140
+ return state.consumed >= provider.quota.limit_tokens;
141
+ }
142
+ /** Dashboard view: window, consumed, countdown, and progress for each provider. */
143
+ snapshot(providers) {
144
+ const t = this.now();
145
+ return providers.flatMap((provider) => {
146
+ if (!provider.quota)
147
+ return [];
148
+ const state = this.current(provider);
149
+ const reset = nextResetAt(provider.quota, state.windowStart, t);
150
+ const limit = provider.quota.limit_tokens;
151
+ return [
152
+ {
153
+ provider: provider.id,
154
+ window: provider.quota.window,
155
+ consumed: state.consumed,
156
+ limit_tokens: limit,
157
+ reset_in_ms: Math.max(0, reset - t),
158
+ pct: limit ? Math.min(1, state.consumed / limit) : undefined,
159
+ exhausted: limit ? state.consumed >= limit : false,
160
+ },
161
+ ];
162
+ });
163
+ }
164
+ }
165
+ //# sourceMappingURL=quota.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"quota.js","sourceRoot":"","sources":["../../src/core/quota.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Mutable holder for the live gateway config, key pool, and quota tracker.
3
+ *
4
+ * Config loads once at boot, but the dashboard edits it at runtime. Routes read
5
+ * `state.config` / `state.pool` / `state.quota` fresh per request (never close
6
+ * over them), so a successful reload swaps in the new config + pool atomically —
7
+ * no restart.
8
+ *
9
+ * reload() validates and persists BEFORE swapping: an invalid edit throws and
10
+ * the old config keeps serving. The pool is rebuilt (cooldown is transient), but
11
+ * the quota tracker is KEPT across reloads — a budget consumed this window must
12
+ * survive a config edit, else editing config would silently reset every quota.
13
+ */
14
+ import { parseConfigText, validateConfig, unmaskSecrets, writeConfigFile, } from "../config.js";
15
+ import { KeyPool } from "./keypool.js";
16
+ import { QuotaTracker } from "./quota.js";
17
+ export class GatewayState {
18
+ configPath;
19
+ _config;
20
+ _pool;
21
+ _quota;
22
+ constructor(configPath, initial, quota) {
23
+ this.configPath = configPath;
24
+ this._config = initial;
25
+ this._pool = new KeyPool();
26
+ this._quota = quota ?? new QuotaTracker();
27
+ }
28
+ get config() {
29
+ return this._config;
30
+ }
31
+ get pool() {
32
+ return this._pool;
33
+ }
34
+ get quota() {
35
+ return this._quota;
36
+ }
37
+ /**
38
+ * Validate edited config text, restore masked secrets from the live config,
39
+ * persist atomically, then swap in a fresh config + pool. Throws without
40
+ * changing anything if validation fails or a masked key can't be resolved —
41
+ * the old config keeps serving. The quota tracker is intentionally preserved.
42
+ */
43
+ reload(text) {
44
+ const parsed = parseConfigText(text);
45
+ const merged = unmaskSecrets(parsed.raw, this._config.raw);
46
+ const next = validateConfig(merged);
47
+ writeConfigFile(this.configPath, next.raw);
48
+ this._config = next;
49
+ this._pool = new KeyPool();
50
+ }
51
+ }
52
+ //# sourceMappingURL=state.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state.js","sourceRoot":"","sources":["../../src/core/state.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAEL,eAAe,EACf,cAAc,EACd,aAAa,EACb,eAAe,GAChB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE1C,MAAM,OAAO,YAAY;IAMJ;IALX,OAAO,CAAgB;IACvB,KAAK,CAAU;IACN,MAAM,CAAe;IAEtC,YACmB,UAAkB,EACnC,OAAsB,EACtB,KAAoB;QAFH,eAAU,GAAV,UAAU,CAAQ;QAInC,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,KAAK,GAAG,IAAI,OAAO,EAAE,CAAC;QAC3B,IAAI,CAAC,MAAM,GAAG,KAAK,IAAI,IAAI,YAAY,EAAE,CAAC;IAC5C,CAAC;IAED,IAAI,MAAM;QACR,OAAO,IAAI,CAAC,OAAO,CAAC;IACtB,CAAC;IAED,IAAI,IAAI;QACN,OAAO,IAAI,CAAC,KAAK,CAAC;IACpB,CAAC;IAED,IAAI,KAAK;QACP,OAAO,IAAI,CAAC,MAAM,CAAC;IACrB,CAAC;IAED;;;;;OAKG;IACH,MAAM,CAAC,IAAY;QACjB,MAAM,MAAM,GAAG,eAAe,CAAC,IAAI,CAAC,CAAC;QACrC,MAAM,MAAM,GAAG,aAAa,CAAC,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QAC3D,MAAM,IAAI,GAAG,cAAc,CAAC,MAAM,CAAC,CAAC;QACpC,eAAe,CAAC,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC;QAC3C,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;QACpB,IAAI,CAAC,KAAK,GAAG,IAAI,OAAO,EAAE,CAAC;IAC7B,CAAC;CACF"}
package/dist/db.js ADDED
@@ -0,0 +1,193 @@
1
+ /**
2
+ * Usage tracking store, backed by the built-in node:sqlite (no native build).
3
+ * One `usage` row per upstream request that produced usage; an optional `logs`
4
+ * table holds request/response summaries for debugging. Unified under DATA_DIR
5
+ * (default ./data).
6
+ */
7
+ import { mkdirSync } from "node:fs";
8
+ import { dirname } from "node:path";
9
+ import { createRequire } from "node:module";
10
+ // node:sqlite is a recent builtin; require it dynamically so bundlers/test
11
+ // transformers that don't yet know the `node:sqlite` specifier don't choke.
12
+ const { DatabaseSync } = createRequire(import.meta.url)("node:sqlite");
13
+ const num = (v) => Number(v ?? 0);
14
+ export class UsageDB {
15
+ db;
16
+ insertUsage;
17
+ insertLog;
18
+ upsertQuota;
19
+ now;
20
+ constructor(path, now = Date.now) {
21
+ if (path !== ":memory:")
22
+ mkdirSync(dirname(path), { recursive: true });
23
+ this.db = new DatabaseSync(path);
24
+ this.db.exec(`
25
+ CREATE TABLE IF NOT EXISTS usage (
26
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
27
+ ts INTEGER NOT NULL,
28
+ alias TEXT NOT NULL,
29
+ provider TEXT NOT NULL,
30
+ model TEXT NOT NULL,
31
+ tokens_in INTEGER NOT NULL DEFAULT 0,
32
+ tokens_out INTEGER NOT NULL DEFAULT 0,
33
+ cached_tokens INTEGER NOT NULL DEFAULT 0,
34
+ cost REAL NOT NULL DEFAULT 0,
35
+ status INTEGER NOT NULL,
36
+ latency_ms INTEGER NOT NULL DEFAULT 0,
37
+ stream INTEGER NOT NULL DEFAULT 0
38
+ );
39
+ CREATE INDEX IF NOT EXISTS idx_usage_ts ON usage(ts);
40
+ CREATE TABLE IF NOT EXISTS logs (
41
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
42
+ ts INTEGER NOT NULL,
43
+ direction TEXT NOT NULL,
44
+ provider TEXT NOT NULL DEFAULT '',
45
+ status INTEGER NOT NULL DEFAULT 0,
46
+ request_summary TEXT NOT NULL DEFAULT '',
47
+ response_summary TEXT NOT NULL DEFAULT ''
48
+ );
49
+ CREATE INDEX IF NOT EXISTS idx_logs_ts ON logs(ts);
50
+ CREATE TABLE IF NOT EXISTS quota_state (
51
+ provider_id TEXT PRIMARY KEY,
52
+ window_start INTEGER NOT NULL,
53
+ consumed INTEGER NOT NULL DEFAULT 0,
54
+ last_reset INTEGER NOT NULL DEFAULT 0
55
+ );
56
+ `);
57
+ this.now = now;
58
+ this.insertUsage = this.db.prepare(`
59
+ INSERT INTO usage (ts, alias, provider, model, tokens_in, tokens_out, cached_tokens, cost, status, latency_ms, stream)
60
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
61
+ `);
62
+ this.insertLog = this.db.prepare(`
63
+ INSERT INTO logs (ts, direction, provider, status, request_summary, response_summary)
64
+ VALUES (?, ?, ?, ?, ?, ?)
65
+ `);
66
+ // upsert keyed on provider_id so each provider keeps one live window row.
67
+ this.upsertQuota = this.db.prepare(`
68
+ INSERT INTO quota_state (provider_id, window_start, consumed, last_reset)
69
+ VALUES (?, ?, ?, ?)
70
+ ON CONFLICT(provider_id) DO UPDATE SET window_start = excluded.window_start,
71
+ consumed = excluded.consumed, last_reset = excluded.last_reset
72
+ `);
73
+ }
74
+ record(row) {
75
+ this.insertUsage.run(row.ts ?? this.now(), row.alias, row.provider, row.model, row.tokens_in, row.tokens_out, row.cached_tokens, row.cost, row.status, row.latency_ms, row.stream);
76
+ }
77
+ log(row) {
78
+ this.insertLog.run(row.ts ?? this.now(), row.direction, row.provider, row.status, row.request_summary, row.response_summary);
79
+ }
80
+ /** Summary over rows with ts >= sinceMs (default: all time). */
81
+ summary(sinceMs = 0) {
82
+ const total = this.db
83
+ .prepare(`SELECT COUNT(*) requests, COALESCE(SUM(tokens_in),0) tokens_in,
84
+ COALESCE(SUM(tokens_out),0) tokens_out, COALESCE(SUM(cost),0) cost
85
+ FROM usage WHERE ts >= ?`)
86
+ .get(sinceMs);
87
+ const by_provider = this.db
88
+ .prepare(`SELECT provider, COUNT(*) requests, COALESCE(SUM(tokens_in),0) tokens_in,
89
+ COALESCE(SUM(tokens_out),0) tokens_out, COALESCE(SUM(cost),0) cost
90
+ FROM usage WHERE ts >= ? GROUP BY provider ORDER BY cost DESC`)
91
+ .all(sinceMs);
92
+ const by_model = this.db
93
+ .prepare(`SELECT alias, model, COUNT(*) requests, COALESCE(SUM(tokens_in),0) tokens_in,
94
+ COALESCE(SUM(tokens_out),0) tokens_out, COALESCE(SUM(cost),0) cost
95
+ FROM usage WHERE ts >= ? GROUP BY alias, model ORDER BY cost DESC`)
96
+ .all(sinceMs);
97
+ return {
98
+ total: {
99
+ requests: num(total.requests),
100
+ tokens_in: num(total.tokens_in),
101
+ tokens_out: num(total.tokens_out),
102
+ cost: num(total.cost),
103
+ },
104
+ by_provider: by_provider.map((r) => ({
105
+ provider: String(r.provider),
106
+ requests: num(r.requests),
107
+ tokens_in: num(r.tokens_in),
108
+ tokens_out: num(r.tokens_out),
109
+ cost: num(r.cost),
110
+ })),
111
+ by_model: by_model.map((r) => ({
112
+ alias: String(r.alias),
113
+ model: String(r.model),
114
+ requests: num(r.requests),
115
+ tokens_in: num(r.tokens_in),
116
+ tokens_out: num(r.tokens_out),
117
+ cost: num(r.cost),
118
+ })),
119
+ };
120
+ }
121
+ /**
122
+ * Bucketed time-series for charts: one point per `bucketMs` interval from
123
+ * `sinceMs` to now, aligned to the bucket boundary, with zero-filled gaps.
124
+ */
125
+ series(sinceMs, bucketMs) {
126
+ const now = this.now();
127
+ const start = Math.floor(sinceMs / bucketMs) * bucketMs;
128
+ const rows = this.db
129
+ .prepare(`SELECT CAST(ts / ? AS INTEGER) * ? AS bucket, COUNT(*) requests,
130
+ COALESCE(SUM(tokens_in),0) tokens_in,
131
+ COALESCE(SUM(tokens_out),0) tokens_out, COALESCE(SUM(cost),0) cost
132
+ FROM usage WHERE ts >= ? GROUP BY bucket ORDER BY bucket`)
133
+ .all(bucketMs, bucketMs, sinceMs);
134
+ const byBucket = new Map();
135
+ for (const r of rows)
136
+ byBucket.set(num(r.bucket), r);
137
+ const out = [];
138
+ for (let t = start; t <= now; t += bucketMs) {
139
+ const r = byBucket.get(t);
140
+ out.push({
141
+ ts: t,
142
+ requests: r ? num(r.requests) : 0,
143
+ tokens_in: r ? num(r.tokens_in) : 0,
144
+ tokens_out: r ? num(r.tokens_out) : 0,
145
+ cost: r ? num(r.cost) : 0,
146
+ });
147
+ }
148
+ return out;
149
+ }
150
+ /** Most recent usage rows, newest first. For the dashboard logs page. */
151
+ recent(limit = 100) {
152
+ const rows = this.db
153
+ .prepare(`SELECT ts, alias, provider, model, tokens_in, tokens_out, cached_tokens,
154
+ cost, status, latency_ms, stream
155
+ FROM usage ORDER BY id DESC LIMIT ?`)
156
+ .all(Math.max(1, Math.min(limit, 1000)));
157
+ return rows.map((r) => ({
158
+ ts: num(r.ts),
159
+ alias: String(r.alias),
160
+ provider: String(r.provider),
161
+ model: String(r.model),
162
+ tokens_in: num(r.tokens_in),
163
+ tokens_out: num(r.tokens_out),
164
+ cached_tokens: num(r.cached_tokens),
165
+ cost: num(r.cost),
166
+ status: num(r.status),
167
+ latency_ms: num(r.latency_ms),
168
+ stream: num(r.stream),
169
+ }));
170
+ }
171
+ // ---- QuotaStore: one live window row per provider (survives restart) ----
172
+ loadQuota() {
173
+ const rows = this.db.prepare(`SELECT provider_id, window_start, consumed FROM quota_state`).all();
174
+ return rows.map((r) => ({
175
+ provider_id: String(r.provider_id),
176
+ window_start: num(r.window_start),
177
+ consumed: num(r.consumed),
178
+ }));
179
+ }
180
+ saveQuota(providerId, windowStart, consumed) {
181
+ this.upsertQuota.run(providerId, windowStart, consumed, this.now());
182
+ }
183
+ close() {
184
+ this.db.close();
185
+ }
186
+ }
187
+ /** Compute USD cost from token counts and per-1M prices. */
188
+ export function computeCost(tokensIn, tokensOut, priceIn, priceOut) {
189
+ const ci = priceIn ? (tokensIn / 1_000_000) * priceIn : 0;
190
+ const co = priceOut ? (tokensOut / 1_000_000) * priceOut : 0;
191
+ return ci + co;
192
+ }
193
+ //# sourceMappingURL=db.js.map
package/dist/db.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"db.js","sourceRoot":"","sources":["../src/db.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,44 @@
1
+ const DEFAULT_TIMEOUT_MS = 3000;
2
+ // POST messages to Headroom /v1/compress; returns compressed messages + stats or null.
3
+ async function callCompress(url, messages, model, timeoutMs, compressUserMessages) {
4
+ const endpoint = `${String(url).replace(/\/$/, "")}/v1/compress`;
5
+ const payload = { messages, model };
6
+ if (compressUserMessages)
7
+ payload.config = { compress_user_messages: true };
8
+ const res = await fetch(endpoint, {
9
+ method: "POST",
10
+ headers: { "Content-Type": "application/json" },
11
+ body: JSON.stringify(payload),
12
+ signal: AbortSignal.timeout(timeoutMs),
13
+ });
14
+ if (!res.ok)
15
+ return null;
16
+ const data = (await res.json());
17
+ if (!Array.isArray(data?.messages))
18
+ return null;
19
+ return data;
20
+ }
21
+ /**
22
+ * Compress messages via the Headroom proxy. Returns the compressed messages +
23
+ * stats, or null on any failure (caller keeps the original messages).
24
+ */
25
+ export async function compressWithHeadroom(messages, { url, model, compressUserMessages, timeoutMs = DEFAULT_TIMEOUT_MS }) {
26
+ if (!url || !Array.isArray(messages) || messages.length === 0)
27
+ return null;
28
+ try {
29
+ return await callCompress(url, messages, model, timeoutMs, compressUserMessages);
30
+ }
31
+ catch {
32
+ return null;
33
+ }
34
+ }
35
+ export function formatHeadroomLog(stats) {
36
+ if (!stats)
37
+ return null;
38
+ const before = stats.tokens_before || 0;
39
+ const after = stats.tokens_after || 0;
40
+ const saved = stats.tokens_saved || 0;
41
+ const pct = before > 0 ? ((saved / before) * 100).toFixed(1) : "0";
42
+ return `saved ${saved} tokens / ${before} (${pct}%) ${after ? `after=${after}` : ""}`.trim();
43
+ }
44
+ //# sourceMappingURL=compress.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compress.js","sourceRoot":"","sources":["../../src/headroom/compress.ts"],"names":[],"mappings":"AAYA,MAAM,kBAAkB,GAAG,IAAI,CAAC;AAmBhC,uFAAuF;AACvF,KAAK,UAAU,YAAY,CACzB,GAAW,EACX,QAA4B,EAC5B,KAAa,EACb,SAAiB,EACjB,oBAA8B;IAE9B,MAAM,QAAQ,GAAG,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,cAAc,CAAC;IACjE,MAAM,OAAO,GAA4B,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;IAC7D,IAAI,oBAAoB;QAAE,OAAO,CAAC,MAAM,GAAG,EAAE,sBAAsB,EAAE,IAAI,EAAE,CAAC;IAC5E,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,QAAQ,EAAE;QAChC,MAAM,EAAE,MAAM;QACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;QAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC;QAC7B,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,SAAS,CAAC;KACvC,CAAC,CAAC;IACH,IAAI,CAAC,GAAG,CAAC,EAAE;QAAE,OAAO,IAAI,CAAC;IACzB,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAkB,CAAC;IACjD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC;QAAE,OAAO,IAAI,CAAC;IAChD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,QAA4B,EAC5B,EAAE,GAAG,EAAE,KAAK,EAAE,oBAAoB,EAAE,SAAS,GAAG,kBAAkB,EAAwB;IAE1F,IAAI,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAC3E,IAAI,CAAC;QACH,OAAO,MAAM,YAAY,CAAC,GAAG,EAAE,QAAQ,EAAE,KAAK,EAAE,SAAS,EAAE,oBAAoB,CAAC,CAAC;IACnF,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,KAA2B;IAC3D,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC;IACxB,MAAM,MAAM,GAAG,KAAK,CAAC,aAAa,IAAI,CAAC,CAAC;IACxC,MAAM,KAAK,GAAG,KAAK,CAAC,YAAY,IAAI,CAAC,CAAC;IACtC,MAAM,KAAK,GAAG,KAAK,CAAC,YAAY,IAAI,CAAC,CAAC;IACtC,MAAM,GAAG,GAAG,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,GAAG,MAAM,CAAC,GAAG,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;IACnE,OAAO,SAAS,KAAK,aAAa,MAAM,KAAK,GAAG,MAAM,KAAK,CAAC,CAAC,CAAC,SAAS,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,EAAE,CAAC;AAC/F,CAAC"}
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Headroom CLI detection. "Headroom" is an external context-compression proxy
3
+ * (a Python tool, `headroom proxy`, default http://localhost:8787) that aigetwey
4
+ * pipes request messages through. This module only DETECTS it — install, python
5
+ * interpreter, and whether a proxy is already reachable.
6
+ *
7
+ * aigetwey's own implementation.
8
+ */
9
+ import { execSync } from "node:child_process";
10
+ import { delimiter } from "node:path";
11
+ const IS_WIN = process.platform === "win32";
12
+ const WHICH_CMD = IS_WIN ? "where" : "which";
13
+ // Extra bin dirs often missing from a packaged PATH (Python installs headroom here).
14
+ const EXTRA_BINS = IS_WIN
15
+ ? [
16
+ `${process.env.LOCALAPPDATA || ""}\\Programs\\Python\\Python313\\Scripts`,
17
+ `${process.env.LOCALAPPDATA || ""}\\Programs\\Python\\Python312\\Scripts`,
18
+ `${process.env.LOCALAPPDATA || ""}\\Programs\\Python\\Python311\\Scripts`,
19
+ `${process.env.APPDATA || ""}\\Python\\Python313\\Scripts`,
20
+ ]
21
+ : [
22
+ "/usr/local/bin",
23
+ "/opt/homebrew/bin",
24
+ `${process.env.HOME || ""}/.local/bin`,
25
+ "/usr/bin",
26
+ "/bin",
27
+ ];
28
+ const EXTENDED_PATH = [...EXTRA_BINS, process.env.PATH || ""].filter(Boolean).join(delimiter);
29
+ const PYTHON_CANDIDATES = ["python3.13", "python3.12", "python3.11", "python3.10", "python3", "python"];
30
+ const MIN_VERSION = [3, 10];
31
+ const HEADROOM_HEALTH_TIMEOUT_MS = 1500;
32
+ const LOOPBACK_HOSTS = new Set(["localhost", "127.0.0.1", "::1", "[::1]", "0.0.0.0"]);
33
+ export const DEFAULT_HEADROOM_URL = process.env.HEADROOM_URL || "http://localhost:8787";
34
+ /** Locate the `headroom` binary, or null if not installed. */
35
+ export function findHeadroomBinary() {
36
+ try {
37
+ const out = execSync(`${WHICH_CMD} headroom`, {
38
+ stdio: ["ignore", "pipe", "ignore"],
39
+ windowsHide: true,
40
+ env: { ...process.env, PATH: EXTENDED_PATH },
41
+ })
42
+ .toString()
43
+ .trim();
44
+ // Windows `where` may return multiple lines — take the first.
45
+ return out ? (out.split(/\r?\n/)[0] ?? "").trim() || null : null;
46
+ }
47
+ catch {
48
+ return null;
49
+ }
50
+ }
51
+ /** Find a Python interpreter >= 3.10 (headroom-ai requires it), or null. */
52
+ export function findPython310() {
53
+ for (const candidate of PYTHON_CANDIDATES) {
54
+ try {
55
+ const ver = execSync(`${candidate} --version`, {
56
+ stdio: ["ignore", "pipe", "ignore"],
57
+ windowsHide: true,
58
+ env: { ...process.env, PATH: EXTENDED_PATH },
59
+ })
60
+ .toString()
61
+ .trim();
62
+ const match = ver.match(/(\d+)\.(\d+)/);
63
+ if (!match)
64
+ continue;
65
+ const major = parseInt(match[1], 10);
66
+ const minor = parseInt(match[2], 10);
67
+ if (major > MIN_VERSION[0] || (major === MIN_VERSION[0] && minor >= MIN_VERSION[1])) {
68
+ return candidate;
69
+ }
70
+ }
71
+ catch {
72
+ // candidate not present, try next
73
+ }
74
+ }
75
+ return null;
76
+ }
77
+ /** Probe a Headroom proxy's /health. */
78
+ export async function probeProxyRunning(url) {
79
+ if (!url)
80
+ return false;
81
+ const base = String(url).replace(/\/$/, "");
82
+ try {
83
+ const res = await fetch(`${base}/health`, { signal: AbortSignal.timeout(HEADROOM_HEALTH_TIMEOUT_MS) });
84
+ return res.ok;
85
+ }
86
+ catch {
87
+ return false;
88
+ }
89
+ }
90
+ export function isLoopbackHeadroomUrl(url) {
91
+ try {
92
+ const parsed = new URL(url);
93
+ return LOOPBACK_HOSTS.has(parsed.hostname);
94
+ }
95
+ catch {
96
+ return false;
97
+ }
98
+ }
99
+ /** Aggregate status for the dashboard: installed, running, python interpreter. */
100
+ export async function getHeadroomStatus(url) {
101
+ const path = findHeadroomBinary();
102
+ const python = findPython310();
103
+ const installed = Boolean(path);
104
+ const running = await probeProxyRunning(url);
105
+ const localUrl = isLoopbackHeadroomUrl(url);
106
+ return { installed, path, running, python, localUrl, canStart: installed && localUrl };
107
+ }
108
+ //# sourceMappingURL=detect.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"detect.js","sourceRoot":"","sources":["../../src/headroom/detect.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC9C,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAEtC,MAAM,MAAM,GAAG,OAAO,CAAC,QAAQ,KAAK,OAAO,CAAC;AAC5C,MAAM,SAAS,GAAG,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC;AAE7C,qFAAqF;AACrF,MAAM,UAAU,GAAG,MAAM;IACvB,CAAC,CAAC;QACE,GAAG,OAAO,CAAC,GAAG,CAAC,YAAY,IAAI,EAAE,wCAAwC;QACzE,GAAG,OAAO,CAAC,GAAG,CAAC,YAAY,IAAI,EAAE,wCAAwC;QACzE,GAAG,OAAO,CAAC,GAAG,CAAC,YAAY,IAAI,EAAE,wCAAwC;QACzE,GAAG,OAAO,CAAC,GAAG,CAAC,OAAO,IAAI,EAAE,8BAA8B;KAC3D;IACH,CAAC,CAAC;QACE,gBAAgB;QAChB,mBAAmB;QACnB,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,EAAE,aAAa;QACtC,UAAU;QACV,MAAM;KACP,CAAC;AAEN,MAAM,aAAa,GAAG,CAAC,GAAG,UAAU,EAAE,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;AAC9F,MAAM,iBAAiB,GAAG,CAAC,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,SAAS,EAAE,QAAQ,CAAC,CAAC;AACxG,MAAM,WAAW,GAAqB,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;AAC9C,MAAM,0BAA0B,GAAG,IAAI,CAAC;AACxC,MAAM,cAAc,GAAG,IAAI,GAAG,CAAC,CAAC,WAAW,EAAE,WAAW,EAAE,KAAK,EAAE,OAAO,EAAE,SAAS,CAAC,CAAC,CAAC;AAEtF,MAAM,CAAC,MAAM,oBAAoB,GAAG,OAAO,CAAC,GAAG,CAAC,YAAY,IAAI,uBAAuB,CAAC;AAWxF,8DAA8D;AAC9D,MAAM,UAAU,kBAAkB;IAChC,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,QAAQ,CAAC,GAAG,SAAS,WAAW,EAAE;YAC5C,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC;YACnC,WAAW,EAAE,IAAI;YACjB,GAAG,EAAE,EAAE,GAAG,OAAO,CAAC,GAAG,EAAE,IAAI,EAAE,aAAa,EAAE;SAC7C,CAAC;aACC,QAAQ,EAAE;aACV,IAAI,EAAE,CAAC;QACV,8DAA8D;QAC9D,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,IAAI,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;IACnE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,aAAa;IAC3B,KAAK,MAAM,SAAS,IAAI,iBAAiB,EAAE,CAAC;QAC1C,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,QAAQ,CAAC,GAAG,SAAS,YAAY,EAAE;gBAC7C,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC;gBACnC,WAAW,EAAE,IAAI;gBACjB,GAAG,EAAE,EAAE,GAAG,OAAO,CAAC,GAAG,EAAE,IAAI,EAAE,aAAa,EAAE;aAC7C,CAAC;iBACC,QAAQ,EAAE;iBACV,IAAI,EAAE,CAAC;YACV,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,CAAC,cAAc,CAAC,CAAC;YACxC,IAAI,CAAC,KAAK;gBAAE,SAAS;YACrB,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAE,EAAE,EAAE,CAAC,CAAC;YACtC,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAE,EAAE,EAAE,CAAC,CAAC;YACtC,IAAI,KAAK,GAAG,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,KAAK,WAAW,CAAC,CAAC,CAAC,IAAI,KAAK,IAAI,WAAW,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;gBACpF,OAAO,SAAS,CAAC;YACnB,CAAC;QACH,CAAC;QAAC,MAAM,CAAC;YACP,kCAAkC;QACpC,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,wCAAwC;AACxC,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,GAAW;IACjD,IAAI,CAAC,GAAG;QAAE,OAAO,KAAK,CAAC;IACvB,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IAC5C,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,IAAI,SAAS,EAAE,EAAE,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,0BAA0B,CAAC,EAAE,CAAC,CAAC;QACvG,OAAO,GAAG,CAAC,EAAE,CAAC;IAChB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED,MAAM,UAAU,qBAAqB,CAAC,GAAW;IAC/C,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC;QAC5B,OAAO,cAAc,CAAC,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IAC7C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED,kFAAkF;AAClF,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,GAAW;IACjD,MAAM,IAAI,GAAG,kBAAkB,EAAE,CAAC;IAClC,MAAM,MAAM,GAAG,aAAa,EAAE,CAAC;IAC/B,MAAM,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAChC,MAAM,OAAO,GAAG,MAAM,iBAAiB,CAAC,GAAG,CAAC,CAAC;IAC7C,MAAM,QAAQ,GAAG,qBAAqB,CAAC,GAAG,CAAC,CAAC;IAC5C,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,SAAS,IAAI,QAAQ,EAAE,CAAC;AACzF,CAAC"}