@chrysb/alphaclaw 0.9.17 → 0.9.18

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.
@@ -11,6 +11,7 @@ const {
11
11
  } = require("./constants");
12
12
 
13
13
  const kGlobalStateKey = "global:login";
14
+ const kLoginThrottleScope = "login";
14
15
 
15
16
  const createLoginAttemptState = (now) => ({
16
17
  attempts: 0,
@@ -48,8 +49,15 @@ const createMemoryLoginThrottleStore = () => {
48
49
  };
49
50
  };
50
51
 
51
- const getClientStateKey = (clientKey) =>
52
- `client:${String(clientKey || "unknown")}`;
52
+ const getClientStateKey = (clientKey, scope = kLoginThrottleScope) => {
53
+ const normalizedClientKey = String(clientKey || "unknown");
54
+ return scope === kLoginThrottleScope
55
+ ? `client:${normalizedClientKey}`
56
+ : `client:${scope}:${normalizedClientKey}`;
57
+ };
58
+
59
+ const getGlobalStateKey = (scope = kLoginThrottleScope) =>
60
+ scope === kLoginThrottleScope ? kGlobalStateKey : `global:${scope}`;
53
61
 
54
62
  const getOrCreateState = (store, stateKey, now) => {
55
63
  const existing = store.get(stateKey);
@@ -153,6 +161,16 @@ const chooseFailureResult = (...results) => {
153
161
 
154
162
  const createLoginThrottle = ({
155
163
  store = createMemoryLoginThrottleStore(),
164
+ scope = kLoginThrottleScope,
165
+ windowMs = kLoginWindowMs,
166
+ maxAttempts = kLoginMaxAttempts,
167
+ baseLockMs = kLoginBaseLockMs,
168
+ maxLockMs = kLoginMaxLockMs,
169
+ globalWindowMs = kLoginGlobalWindowMs,
170
+ globalMaxAttempts = kLoginGlobalMaxAttempts,
171
+ globalBaseLockMs = kLoginGlobalBaseLockMs,
172
+ globalMaxLockMs = kLoginGlobalMaxLockMs,
173
+ stateTtlMs = kLoginStateTtlMs,
156
174
  } = {}) => {
157
175
  const runExclusive =
158
176
  typeof store.runExclusive === "function"
@@ -161,13 +179,14 @@ const createLoginThrottle = ({
161
179
 
162
180
  const getOrCreateLoginAttemptState = (clientKey, now) =>
163
181
  runExclusive(() => {
164
- const clientStateKey = getClientStateKey(clientKey);
182
+ const clientStateKey = getClientStateKey(clientKey, scope);
183
+ const globalStateKey = getGlobalStateKey(scope);
165
184
  return {
166
185
  clientKey,
167
186
  clientStateKey,
168
- globalStateKey: kGlobalStateKey,
187
+ globalStateKey,
169
188
  client: getOrCreateState(store, clientStateKey, now),
170
- global: getOrCreateState(store, kGlobalStateKey, now),
189
+ global: getOrCreateState(store, globalStateKey, now),
171
190
  };
172
191
  });
173
192
 
@@ -175,19 +194,19 @@ const createLoginThrottle = ({
175
194
  runExclusive(() => {
176
195
  const clientStateKey =
177
196
  stateBundle?.clientStateKey ||
178
- getClientStateKey(stateBundle?.clientKey);
179
- const globalStateKey = stateBundle?.globalStateKey || kGlobalStateKey;
197
+ getClientStateKey(stateBundle?.clientKey, scope);
198
+ const globalStateKey = stateBundle?.globalStateKey || getGlobalStateKey(scope);
180
199
  const clientResult = evaluateState({
181
200
  store,
182
201
  stateKey: clientStateKey,
183
202
  now,
184
- windowMs: kLoginWindowMs,
203
+ windowMs,
185
204
  });
186
205
  const globalResult = evaluateState({
187
206
  store,
188
207
  stateKey: globalStateKey,
189
208
  now,
190
- windowMs: kLoginGlobalWindowMs,
209
+ windowMs: globalWindowMs,
191
210
  });
192
211
  if (stateBundle) {
193
212
  stateBundle.client = clientResult.state;
@@ -200,25 +219,25 @@ const createLoginThrottle = ({
200
219
  runExclusive(() => {
201
220
  const clientStateKey =
202
221
  stateBundle?.clientStateKey ||
203
- getClientStateKey(stateBundle?.clientKey);
204
- const globalStateKey = stateBundle?.globalStateKey || kGlobalStateKey;
222
+ getClientStateKey(stateBundle?.clientKey, scope);
223
+ const globalStateKey = stateBundle?.globalStateKey || getGlobalStateKey(scope);
205
224
  const clientResult = recordStateFailure({
206
225
  store,
207
226
  stateKey: clientStateKey,
208
227
  now,
209
- windowMs: kLoginWindowMs,
210
- maxAttempts: kLoginMaxAttempts,
211
- baseLockMs: kLoginBaseLockMs,
212
- maxLockMs: kLoginMaxLockMs,
228
+ windowMs,
229
+ maxAttempts,
230
+ baseLockMs,
231
+ maxLockMs,
213
232
  });
214
233
  const globalResult = recordStateFailure({
215
234
  store,
216
235
  stateKey: globalStateKey,
217
236
  now,
218
- windowMs: kLoginGlobalWindowMs,
219
- maxAttempts: kLoginGlobalMaxAttempts,
220
- baseLockMs: kLoginGlobalBaseLockMs,
221
- maxLockMs: kLoginGlobalMaxLockMs,
237
+ windowMs: globalWindowMs,
238
+ maxAttempts: globalMaxAttempts,
239
+ baseLockMs: globalBaseLockMs,
240
+ maxLockMs: globalMaxLockMs,
222
241
  });
223
242
  if (stateBundle) {
224
243
  stateBundle.client = clientResult.state;
@@ -230,8 +249,8 @@ const createLoginThrottle = ({
230
249
  const recordLoginSuccess = (clientKey) => {
231
250
  if (!clientKey) return;
232
251
  runExclusive(() => {
233
- store.delete(getClientStateKey(clientKey));
234
- store.delete(kGlobalStateKey);
252
+ store.delete(getClientStateKey(clientKey, scope));
253
+ store.delete(getGlobalStateKey(scope));
235
254
  });
236
255
  };
237
256
 
@@ -245,7 +264,7 @@ const createLoginThrottle = ({
245
264
  }
246
265
  const state = normalizeState(rawState, now);
247
266
  if (state.lockUntil > now) continue;
248
- if (now - state.lastSeenAt > kLoginStateTtlMs) {
267
+ if (now - state.lastSeenAt > stateTtlMs) {
249
268
  store.delete(stateKey);
250
269
  }
251
270
  }
@@ -4,6 +4,7 @@ const {
4
4
  ensurePluginAllowed,
5
5
  ensureUsageTrackerPluginEntry,
6
6
  } = require("../usage-tracker-config");
7
+ const { isOpenAiCompatApiEnabled } = require("../alphaclaw-config");
7
8
 
8
9
  const kDefaultToolsProfile = "full";
9
10
  const kBootstrapExtraFiles = [
@@ -133,16 +134,29 @@ const buildOnboardArgs = ({
133
134
  return onboardArgs;
134
135
  };
135
136
 
136
- const ensureManagedConfigShell = (cfg) => {
137
+ const ensureManagedConfigShell = (cfg, { openAiCompatApiEnabled = false } = {}) => {
137
138
  if (!cfg.channels) cfg.channels = {};
138
139
  ensurePluginsShell(cfg);
139
140
  if (!cfg.commands) cfg.commands = {};
140
141
  if (!cfg.tools) cfg.tools = {};
142
+ if (!cfg.gateway) cfg.gateway = {};
141
143
  if (!cfg.hooks) cfg.hooks = {};
142
144
  if (!cfg.hooks.internal) cfg.hooks.internal = {};
143
145
  if (!cfg.hooks.internal.entries) cfg.hooks.internal.entries = {};
144
146
  cfg.commands.restart = true;
145
147
  cfg.tools.profile = kDefaultToolsProfile;
148
+ if (openAiCompatApiEnabled) {
149
+ if (!cfg.gateway.http) cfg.gateway.http = {};
150
+ if (!cfg.gateway.http.endpoints) cfg.gateway.http.endpoints = {};
151
+ cfg.gateway.http.endpoints.chatCompletions = {
152
+ ...(cfg.gateway.http.endpoints.chatCompletions || {}),
153
+ enabled: true,
154
+ };
155
+ cfg.gateway.http.endpoints.responses = {
156
+ ...(cfg.gateway.http.endpoints.responses || {}),
157
+ enabled: true,
158
+ };
159
+ }
146
160
  cfg.hooks.internal.enabled = true;
147
161
  cfg.hooks.internal.entries["bootstrap-extra-files"] = {
148
162
  ...(cfg.hooks.internal.entries["bootstrap-extra-files"] || {}),
@@ -224,7 +238,12 @@ const writeSanitizedOpenclawConfig = ({
224
238
  }) => {
225
239
  const configPath = `${openclawDir}/openclaw.json`;
226
240
  const cfg = JSON.parse(fs.readFileSync(configPath, "utf8"));
227
- ensureManagedConfigShell(cfg);
241
+ ensureManagedConfigShell(cfg, {
242
+ openAiCompatApiEnabled: isOpenAiCompatApiEnabled({
243
+ fsModule: fs,
244
+ openclawDir,
245
+ }),
246
+ });
228
247
  applyFreshOnboardingChannels({
229
248
  cfg,
230
249
  varMap,
@@ -256,7 +275,12 @@ const writeManagedImportOpenclawConfig = ({
256
275
  }) => {
257
276
  const configPath = `${openclawDir}/openclaw.json`;
258
277
  const cfg = JSON.parse(fs.readFileSync(configPath, "utf8"));
259
- ensureManagedConfigShell(cfg);
278
+ ensureManagedConfigShell(cfg, {
279
+ openAiCompatApiEnabled: isOpenAiCompatApiEnabled({
280
+ fsModule: fs,
281
+ openclawDir,
282
+ }),
283
+ });
260
284
 
261
285
  ensureUsageTrackerPluginEntry(cfg);
262
286
 
@@ -1,7 +1,202 @@
1
+ const crypto = require("crypto");
2
+ const http = require("http");
3
+ const https = require("https");
4
+ const { URL } = require("url");
5
+
6
+ const kOpenAiCompatProxyPathPattern =
7
+ /^\/v1\/(?:chat\/completions|responses|embeddings|models(?:\/[^/?#]+)?)$/;
8
+ const kHopByHopResponseHeaders = new Set([
9
+ "connection",
10
+ "keep-alive",
11
+ "proxy-authenticate",
12
+ "proxy-authorization",
13
+ "te",
14
+ "trailer",
15
+ "trailers",
16
+ "transfer-encoding",
17
+ "upgrade",
18
+ ]);
19
+ // Strip these even though they're not hop-by-hop: an OpenAI-compatible client
20
+ // (e.g. Sure's external assistant) has no business receiving cookies from the
21
+ // gateway, and a stray Set-Cookie crossing the AlphaClaw boundary would be a
22
+ // real leak.
23
+ const kAlwaysStrippedResponseHeaders = new Set(["set-cookie"]);
24
+
25
+ const extractBearerToken = (authorization) => {
26
+ const match = String(authorization || "").match(/^Bearer\s+(.+)$/i);
27
+ return match ? match[1].trim() : "";
28
+ };
29
+
30
+ const getApiAuthThrottleState = (authThrottle, req, now) => {
31
+ if (!authThrottle || typeof authThrottle.getOrCreateLoginAttemptState !== "function") {
32
+ return null;
33
+ }
34
+ const clientKey =
35
+ typeof authThrottle.getClientKey === "function"
36
+ ? authThrottle.getClientKey(req)
37
+ : req.ip || req.socket?.remoteAddress || "unknown";
38
+ return {
39
+ clientKey,
40
+ state: authThrottle.getOrCreateLoginAttemptState(clientKey, now),
41
+ };
42
+ };
43
+
44
+ const sendTooManyAuthAttempts = (res, retryAfterSec = 1) => {
45
+ const normalizedRetryAfterSec = Math.max(1, Math.ceil(Number(retryAfterSec) || 1));
46
+ res.set("Retry-After", String(normalizedRetryAfterSec));
47
+ return res.status(429).json({
48
+ error: "Too many attempts. Try again shortly.",
49
+ retryAfterSec: normalizedRetryAfterSec,
50
+ });
51
+ };
52
+
53
+ const timingSafeStringEqual = (left, right) => {
54
+ const leftBuffer = Buffer.from(String(left || ""), "utf8");
55
+ const rightBuffer = Buffer.from(String(right || ""), "utf8");
56
+ return (
57
+ leftBuffer.length === rightBuffer.length &&
58
+ crypto.timingSafeEqual(leftBuffer, rightBuffer)
59
+ );
60
+ };
61
+
62
+ const extractBodyBuffer = (req) => {
63
+ if (Buffer.isBuffer(req.body)) return req.body;
64
+ if (typeof req.body === "string") return Buffer.from(req.body, "utf8");
65
+ if (req.body && typeof req.body === "object") {
66
+ return Buffer.from(JSON.stringify(req.body), "utf8");
67
+ }
68
+ return Buffer.alloc(0);
69
+ };
70
+
71
+ const createGatewayProxyHeaders = ({ reqHeaders, bodyBuffer }) => {
72
+ const headers = { ...(reqHeaders || {}) };
73
+ delete headers.host;
74
+ delete headers.connection;
75
+ delete headers["content-length"];
76
+ delete headers["transfer-encoding"];
77
+ // Express has already parsed and (if gzip/deflate) inflated the body, so
78
+ // the bytes we reserialize are plain JSON. Forwarding the original
79
+ // Content-Encoding would tell the gateway to gunzip plain text and fail.
80
+ delete headers["content-encoding"];
81
+ delete headers.cookie;
82
+ if (bodyBuffer.length > 0) {
83
+ headers["content-length"] = String(bodyBuffer.length);
84
+ if (!headers["content-type"]) headers["content-type"] = "application/json";
85
+ }
86
+ return headers;
87
+ };
88
+
89
+ const proxyOpenAiCompatRequest = ({
90
+ req,
91
+ res,
92
+ getGatewayUrl,
93
+ getGatewayToken,
94
+ openAiCompatApiThrottle,
95
+ }) => {
96
+ const now = Date.now();
97
+ const throttleState = getApiAuthThrottleState(
98
+ openAiCompatApiThrottle,
99
+ req,
100
+ now,
101
+ );
102
+ if (
103
+ throttleState &&
104
+ typeof openAiCompatApiThrottle.evaluateLoginThrottle === "function"
105
+ ) {
106
+ const throttle = openAiCompatApiThrottle.evaluateLoginThrottle(
107
+ throttleState.state,
108
+ now,
109
+ );
110
+ if (throttle.blocked) {
111
+ return sendTooManyAuthAttempts(res, throttle.retryAfterSec);
112
+ }
113
+ }
114
+
115
+ const bearerToken = extractBearerToken(req.headers.authorization);
116
+ const expectedGatewayToken = String(getGatewayToken?.() || "").trim();
117
+ if (
118
+ !bearerToken ||
119
+ !expectedGatewayToken ||
120
+ !timingSafeStringEqual(bearerToken, expectedGatewayToken)
121
+ ) {
122
+ if (
123
+ throttleState &&
124
+ typeof openAiCompatApiThrottle.recordLoginFailure === "function"
125
+ ) {
126
+ const failure = openAiCompatApiThrottle.recordLoginFailure(
127
+ throttleState.state,
128
+ now,
129
+ );
130
+ if (failure.locked) {
131
+ return sendTooManyAuthAttempts(res, failure.retryAfterSec);
132
+ }
133
+ }
134
+ return res.status(401).json({ error: "Unauthorized" });
135
+ }
136
+ if (
137
+ throttleState?.clientKey &&
138
+ typeof openAiCompatApiThrottle?.recordLoginSuccess === "function"
139
+ ) {
140
+ openAiCompatApiThrottle.recordLoginSuccess(throttleState.clientKey);
141
+ }
142
+
143
+ let gateway;
144
+ try {
145
+ gateway = new URL(getGatewayUrl());
146
+ } catch {
147
+ return res.status(502).json({ error: "Gateway unavailable" });
148
+ }
149
+
150
+ const bodyBuffer = extractBodyBuffer(req);
151
+ const protocolClient = gateway.protocol === "https:" ? https : http;
152
+ const headers = createGatewayProxyHeaders({
153
+ reqHeaders: req.headers,
154
+ bodyBuffer,
155
+ });
156
+ headers.authorization = `Bearer ${bearerToken}`;
157
+
158
+ const requestOptions = {
159
+ protocol: gateway.protocol,
160
+ hostname: gateway.hostname,
161
+ port: gateway.port,
162
+ method: req.method,
163
+ path: req.originalUrl || req.url,
164
+ headers,
165
+ };
166
+
167
+ const proxyReq = protocolClient.request(requestOptions, (proxyRes) => {
168
+ res.statusCode = proxyRes.statusCode || 502;
169
+ for (const [key, value] of Object.entries(proxyRes.headers || {})) {
170
+ if (value == null) continue;
171
+ const lowerKey = key.toLowerCase();
172
+ if (kHopByHopResponseHeaders.has(lowerKey)) continue;
173
+ if (kAlwaysStrippedResponseHeaders.has(lowerKey)) continue;
174
+ res.setHeader(key, value);
175
+ }
176
+ proxyRes.pipe(res);
177
+ });
178
+
179
+ proxyReq.on("error", () => {
180
+ if (!res.headersSent) {
181
+ res.status(502).json({ error: "Gateway unavailable" });
182
+ } else {
183
+ res.end();
184
+ }
185
+ });
186
+
187
+ if (bodyBuffer.length > 0) {
188
+ proxyReq.write(bodyBuffer);
189
+ }
190
+ proxyReq.end();
191
+ };
192
+
1
193
  const registerProxyRoutes = ({
2
194
  app,
3
195
  proxy,
4
196
  getGatewayUrl,
197
+ getGatewayToken,
198
+ isOpenAiCompatApiEnabled = () => true,
199
+ openAiCompatApiThrottle = null,
5
200
  SETUP_API_PREFIXES,
6
201
  requireAuth,
7
202
  oauthCallbackMiddleware,
@@ -29,10 +224,33 @@ const registerProxyRoutes = ({
29
224
  app.all(kHooksPathPattern, webhookMiddleware);
30
225
  app.all(kWebhookPathPattern, webhookMiddleware);
31
226
 
227
+ app.all(kOpenAiCompatProxyPathPattern, (req, res) => {
228
+ if (!isOpenAiCompatApiEnabled()) {
229
+ return res.status(404).json({ error: "Not found" });
230
+ }
231
+ return proxyOpenAiCompatRequest({
232
+ req,
233
+ res,
234
+ getGatewayUrl,
235
+ getGatewayToken,
236
+ openAiCompatApiThrottle,
237
+ });
238
+ });
239
+
32
240
  app.all(kApiPathPattern, (req, res, next) => {
33
241
  if (SETUP_API_PREFIXES.some((p) => req.path.startsWith(p))) return next();
34
242
  proxy.web(req, res, { target: getGatewayUrl() });
35
243
  });
36
244
  };
37
245
 
38
- module.exports = { registerProxyRoutes };
246
+ module.exports = {
247
+ kOpenAiCompatProxyPathPattern,
248
+ registerProxyRoutes,
249
+ // Exported for tests.
250
+ __testing: {
251
+ createGatewayProxyHeaders,
252
+ extractBearerToken,
253
+ kHopByHopResponseHeaders,
254
+ kAlwaysStrippedResponseHeaders,
255
+ },
256
+ };
@@ -1,5 +1,9 @@
1
1
  const { buildManagedPaths } = require("../internal-files-migration");
2
2
  const { readOpenclawConfig } = require("../openclaw-config");
3
+ const {
4
+ readAlphaclawConfig,
5
+ updateOpenAiCompatApiFeature,
6
+ } = require("../alphaclaw-config");
3
7
  const https = require("https");
4
8
 
5
9
  const registerSystemRoutes = ({
@@ -26,6 +30,8 @@ const registerSystemRoutes = ({
26
30
  authProfiles,
27
31
  watchdog,
28
32
  doctorService,
33
+ ensureGatewayProxyConfig,
34
+ getBaseUrl,
29
35
  }) => {
30
36
  let envRestartPending = false;
31
37
  let openclawSecretRuntimePromise = null;
@@ -590,6 +596,10 @@ const registerSystemRoutes = ({
590
596
  repo,
591
597
  openclawVersion,
592
598
  alphaclawVersion,
599
+ alphaclaw: readAlphaclawConfig({
600
+ fsModule: fs,
601
+ openclawDir: OPENCLAW_DIR,
602
+ }),
593
603
  syncCron: getSystemCronStatus(),
594
604
  };
595
605
  };
@@ -668,6 +678,57 @@ const registerSystemRoutes = ({
668
678
  res.json({ ok: true, syncCron: status });
669
679
  });
670
680
 
681
+ app.get("/api/alphaclaw/config", (req, res) => {
682
+ res.json({
683
+ ok: true,
684
+ config: readAlphaclawConfig({
685
+ fsModule: fs,
686
+ openclawDir: OPENCLAW_DIR,
687
+ }),
688
+ });
689
+ });
690
+
691
+ app.put("/api/alphaclaw/config/features/openai-compat-api", async (req, res) => {
692
+ const { enabled } = req.body || {};
693
+ if (typeof enabled !== "boolean") {
694
+ return res
695
+ .status(400)
696
+ .json({ ok: false, error: "enabled must be a boolean" });
697
+ }
698
+
699
+ try {
700
+ const { config, changed } = updateOpenAiCompatApiFeature({
701
+ fsModule: fs,
702
+ openclawDir: OPENCLAW_DIR,
703
+ enabled,
704
+ });
705
+ let gatewayConfigChanged = false;
706
+ if (enabled && isOnboarded() && typeof ensureGatewayProxyConfig === "function") {
707
+ gatewayConfigChanged = ensureGatewayProxyConfig(getBaseUrl?.(req));
708
+ if (gatewayConfigChanged && restartRequiredState?.markRequired) {
709
+ restartRequiredState.markRequired("openai_compat_api_enabled");
710
+ }
711
+ }
712
+ const snapshot =
713
+ typeof restartRequiredState?.getSnapshot === "function"
714
+ ? await restartRequiredState.getSnapshot()
715
+ : null;
716
+ res.json({
717
+ ok: true,
718
+ changed,
719
+ gatewayConfigChanged,
720
+ config,
721
+ restartRequired:
722
+ Boolean(snapshot?.restartRequired) || (envRestartPending && isOnboarded()),
723
+ });
724
+ } catch (err) {
725
+ res.status(500).json({
726
+ ok: false,
727
+ error: err.message || "Could not update AlphaClaw config",
728
+ });
729
+ }
730
+ });
731
+
671
732
  app.get("/api/alphaclaw/version", async (req, res) => {
672
733
  const refresh = String(req.query.refresh || "") === "1";
673
734
  const status = await alphaclawVersionService.getVersionStatus(refresh);
package/lib/server.js CHANGED
@@ -148,6 +148,9 @@ const {
148
148
  const {
149
149
  ensureOpenclawStartupEnv,
150
150
  } = require("./server/openclaw-runtime-env");
151
+ const {
152
+ isOpenAiCompatApiEnabled,
153
+ } = require("./server/alphaclaw-config");
151
154
 
152
155
  const { PORT, kTrustProxyHops, SETUP_API_PREFIXES } = constants;
153
156
 
@@ -165,6 +168,18 @@ const app = express();
165
168
  app.set("trust proxy", kTrustProxyHops);
166
169
  app.use(["/webhook", "/hooks"], express.raw({ type: "*/*", limit: "5mb" }));
167
170
  app.use("/gmail-pubsub", express.raw({ type: "*/*", limit: "5mb" }));
171
+ const openAiCompatJsonParser = express.json({ limit: "50mb" });
172
+ const isOpenAiCompatApiCurrentlyEnabled = () =>
173
+ isOpenAiCompatApiEnabled({
174
+ fsModule: fs,
175
+ openclawDir: constants.OPENCLAW_DIR,
176
+ });
177
+ app.use("/v1", (req, res, next) => {
178
+ if (!isOpenAiCompatApiCurrentlyEnabled()) {
179
+ return res.status(404).json({ error: "Not found" });
180
+ }
181
+ return openAiCompatJsonParser(req, res, next);
182
+ });
168
183
  app.use(express.json({ limit: "5mb" }));
169
184
 
170
185
  const proxy = httpProxy.createProxyServer({
@@ -190,8 +205,25 @@ const agentsService = createAgentsService({
190
205
  restartGateway: () => restartGatewayWithReload(reloadEnv),
191
206
  clawCmd,
192
207
  });
208
+ const loginThrottleStore = createLoginThrottleStore();
193
209
  const loginThrottle = {
194
- ...createLoginThrottle({ store: createLoginThrottleStore() }),
210
+ ...createLoginThrottle({ store: loginThrottleStore }),
211
+ getClientKey,
212
+ };
213
+ const openAiCompatApiThrottle = {
214
+ ...createLoginThrottle({
215
+ store: loginThrottleStore,
216
+ scope: "openai-compat-api",
217
+ windowMs: constants.kOpenAiCompatApiRateWindowMs,
218
+ maxAttempts: constants.kOpenAiCompatApiRateMaxAttempts,
219
+ baseLockMs: constants.kOpenAiCompatApiRateBaseLockMs,
220
+ maxLockMs: constants.kOpenAiCompatApiRateMaxLockMs,
221
+ globalWindowMs: constants.kOpenAiCompatApiRateGlobalWindowMs,
222
+ globalMaxAttempts: constants.kOpenAiCompatApiRateGlobalMaxAttempts,
223
+ globalBaseLockMs: constants.kOpenAiCompatApiRateGlobalBaseLockMs,
224
+ globalMaxLockMs: constants.kOpenAiCompatApiRateGlobalMaxLockMs,
225
+ stateTtlMs: constants.kOpenAiCompatApiRateStateTtlMs,
226
+ }),
195
227
  getClientKey,
196
228
  };
197
229
  const resolveSetupUrl = () =>
@@ -318,6 +350,8 @@ const {
318
350
  resolveGithubRepoUrl,
319
351
  resolveModelProvider,
320
352
  ensureGatewayProxyConfig,
353
+ isOpenAiCompatApiEnabled: isOpenAiCompatApiCurrentlyEnabled,
354
+ openAiCompatApiThrottle,
321
355
  getBaseUrl,
322
356
  startGateway,
323
357
  ensureManagedExecDefaults: () =>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chrysb/alphaclaw",
3
- "version": "0.9.17",
3
+ "version": "0.9.18",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },