openclaw-abacusai-auth 1.2.4 → 1.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { existsSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
1
+ import { existsSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
2
2
  import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
3
3
  import { homedir } from "node:os";
4
4
  import { join } from "node:path";
@@ -15,8 +15,9 @@ const DEFAULT_MAX_TOKENS = 8192;
15
15
 
16
16
  // Proxy configuration
17
17
  const PROXY_HOST = "127.0.0.1";
18
- // Dynamic port - will be assigned when proxy starts
19
- let proxyPort = 0;
18
+ // Fixed port for the proxy so the baseUrl saved at auth time always works
19
+ const PROXY_PORT_DEFAULT = 18862;
20
+ let proxyPort = PROXY_PORT_DEFAULT;
20
21
 
21
22
  // Models available on AbacusAI RouteLLM endpoint (OpenAI-compatible, with
22
23
  // function calling support). Verified 2026-02.
@@ -269,6 +270,8 @@ async function validateApiKey(
269
270
 
270
271
  let proxyServer: ReturnType<typeof createServer> | null = null;
271
272
  let proxyApiKey = "";
273
+ let activeProxyRequests = 0;
274
+ let proxyShuttingDown = false;
272
275
 
273
276
  function readBody(req: IncomingMessage): Promise<Buffer> {
274
277
  return new Promise((resolve, reject) => {
@@ -443,6 +446,13 @@ function normalizeToolsForRouteLLM(tools: unknown[]): unknown[] {
443
446
  fn.parameters = cleanSchema(fn.parameters);
444
447
  }
445
448
 
449
+ // RouteLLM REQUIRES every tool to have a `parameters` field.
450
+ // If a tool has no parameters (e.g. cognitive_assess, flare_plan),
451
+ // add a default empty object schema.
452
+ if (!fn.parameters) {
453
+ fn.parameters = { type: "object", properties: {} };
454
+ }
455
+
446
456
  copy.function = fn;
447
457
 
448
458
  // Promote name and parameters to top level for RouteLLM
@@ -546,6 +556,19 @@ function normalizeResponseToolCalls(json: Record<string, unknown>): Record<strin
546
556
  }
547
557
 
548
558
  async function handleProxyRequest(req: IncomingMessage, res: ServerResponse) {
559
+ if (proxyShuttingDown) {
560
+ sendJsonResponse(res, 503, { error: { message: "Proxy is shutting down", type: "service_unavailable" } });
561
+ return;
562
+ }
563
+ activeProxyRequests++;
564
+ try {
565
+ await handleProxyRequestInner(req, res);
566
+ } finally {
567
+ activeProxyRequests--;
568
+ }
569
+ }
570
+
571
+ async function handleProxyRequestInner(req: IncomingMessage, res: ServerResponse) {
549
572
  const path = req.url ?? "/";
550
573
  const target = `${ROUTELLM_BASE}${path}`;
551
574
  const headers: Record<string, string> = {
@@ -559,6 +582,8 @@ async function handleProxyRequest(req: IncomingMessage, res: ServerResponse) {
559
582
  const parsed = JSON.parse(raw.toString()) as Record<string, unknown>;
560
583
  // Normalize tools for RouteLLM: remove `strict` field, clean schemas
561
584
  // (remove patternProperties, add additionalProperties: false, etc.)
585
+
586
+
562
587
  if (Array.isArray(parsed.tools)) {
563
588
  parsed.tools = normalizeToolsForRouteLLM(parsed.tools);
564
589
  }
@@ -677,18 +702,76 @@ function startProxy(apiKey: string): Promise<void> {
677
702
  sendJsonResponse(res, 500, { error: { message: String(err) } });
678
703
  });
679
704
  });
680
- // Use port 0 to let the OS assign a random available port
681
- proxyServer.listen(0, PROXY_HOST, () => {
682
- const addr = proxyServer?.address();
683
- if (addr && typeof addr === "object") {
684
- proxyPort = addr.port;
685
- }
686
- console.log(`[abacusai] proxy listening on http://${PROXY_HOST}:${proxyPort}`);
705
+
706
+ // Try fixed port first, then retry with port+1, +2, etc.
707
+ const tryListen = (port: number, attempt: number) => {
708
+ proxyServer!.listen(port, PROXY_HOST, () => {
709
+ proxyPort = port;
710
+ console.log(`[abacusai] proxy listening on http://${PROXY_HOST}:${proxyPort}`);
711
+ resolve();
712
+ });
713
+ proxyServer!.once("error", (err: NodeJS.ErrnoException) => {
714
+ if (err.code === "EADDRINUSE" && attempt < 10) {
715
+ console.log(`[abacusai] port ${port} in use, trying ${port + 1}...`);
716
+ proxyServer!.removeAllListeners("error");
717
+ proxyServer!.close(() => {
718
+ proxyServer = createServer((req, res) => {
719
+ handleProxyRequest(req, res).catch((e) => {
720
+ console.error("[abacusai] proxy error:", e);
721
+ sendJsonResponse(res, 500, { error: { message: String(e) } });
722
+ });
723
+ });
724
+ tryListen(port + 1, attempt + 1);
725
+ });
726
+ } else {
727
+ reject(err);
728
+ }
729
+ });
730
+ };
731
+
732
+ tryListen(PROXY_PORT_DEFAULT, 0);
733
+ });
734
+ }
735
+
736
+ /**
737
+ * Gracefully stop the RouteLLM proxy server.
738
+ * 1. Stop accepting new connections
739
+ * 2. Wait for all in-flight requests to complete (up to 10s timeout)
740
+ * 3. Close the server and release the port
741
+ */
742
+ function stopProxy(): Promise<void> {
743
+ return new Promise((resolve) => {
744
+ if (!proxyServer) {
745
+ resolve();
746
+ return;
747
+ }
748
+
749
+ proxyShuttingDown = true;
750
+ console.log(`[abacusai] Proxy shutting down (${activeProxyRequests} active requests)...`);
751
+
752
+ // Stop accepting new connections immediately
753
+ proxyServer.close(() => {
754
+ console.log("[abacusai] Proxy server closed, port released.");
755
+ proxyServer = null;
756
+ proxyShuttingDown = false;
757
+ activeProxyRequests = 0;
687
758
  resolve();
688
759
  });
689
- proxyServer.on("error", (err: NodeJS.ErrnoException) => {
690
- reject(err);
691
- });
760
+
761
+ // Force-close after 10s if requests don't drain
762
+ const forceTimeout = setTimeout(() => {
763
+ console.warn(`[abacusai] Force-closing proxy (${activeProxyRequests} requests still active after 10s).`);
764
+ proxyServer?.closeAllConnections?.();
765
+ }, 10_000);
766
+
767
+ // Poll for active requests to finish, resolve early if all done
768
+ const drainInterval = setInterval(() => {
769
+ if (activeProxyRequests <= 0) {
770
+ clearInterval(drainInterval);
771
+ clearTimeout(forceTimeout);
772
+ // server.close callback will resolve
773
+ }
774
+ }, 200);
692
775
  });
693
776
  }
694
777
 
@@ -742,7 +825,9 @@ function updateBaseUrlInConfig(): void {
742
825
  const configPath = join(stateDir, "openclaw.json");
743
826
  if (!existsSync(configPath)) return;
744
827
 
745
- const raw = readFileSync(configPath, "utf-8");
828
+ let raw = readFileSync(configPath, "utf-8");
829
+ // Strip UTF-8 BOM if present
830
+ if (raw.charCodeAt(0) === 0xFEFF) raw = raw.slice(1);
746
831
  const config = JSON.parse(raw);
747
832
  const currentUrl = config?.models?.providers?.abacusai?.baseUrl;
748
833
 
@@ -783,18 +868,39 @@ interface PluginAuthContext {
783
868
  }
784
869
 
785
870
  const abacusaiPlugin = {
786
- id: "abacusai-auth",
871
+ id: "openclaw-abacusai-auth",
787
872
  name: "AbacusAI Auth",
788
873
  description: "AbacusAI RouteLLM provider plugin with direct connection and schema normalization",
789
874
  configSchema: emptyPluginConfigSchema(),
790
875
  register(api: unknown) {
791
876
  const pluginApi = api as {
792
877
  registerProvider: (config: unknown) => void;
878
+ registerHook?: (events: string | string[], handler: Function, opts?: { name?: string }) => void;
793
879
  config?: {
794
880
  models?: { providers?: { abacusai?: { compat?: { supportsStrictMode?: boolean } } } };
795
881
  };
796
882
  };
797
883
 
884
+ // ================================================================
885
+ // Register gateway_stop hook for graceful proxy shutdown
886
+ // ================================================================
887
+ if (typeof pluginApi.registerHook === "function") {
888
+ pluginApi.registerHook(
889
+ "gateway_stop",
890
+ async () => {
891
+ await stopProxy();
892
+ },
893
+ { name: "openclaw-abacusai-auth:gateway-stop" },
894
+ );
895
+ }
896
+
897
+ // Fallback: handle process signals if gateway_stop hook is unavailable
898
+ const shutdownHandler = () => {
899
+ stopProxy().then(() => process.exit(0));
900
+ };
901
+ process.once("SIGTERM", shutdownHandler);
902
+ process.once("SIGINT", shutdownHandler);
903
+
798
904
  // Use local proxy mode to handle schema cleaning internally
799
905
  // This is required because OpenClaw core may not support requiresCleanSchema yet
800
906
  // The proxy normalizes tool schemas before forwarding to RouteLLM
@@ -934,7 +1040,7 @@ const abacusaiPlugin = {
934
1040
  // and adding `additionalProperties: false`
935
1041
  baseUrl: `http://${PROXY_HOST}:${proxyPort}`,
936
1042
  api: "openai-completions",
937
- auth: "token",
1043
+ apiKey: "abacusai-proxy",
938
1044
  models: modelIds.map((id) => buildModelDefinition(id)),
939
1045
  },
940
1046
  },
@@ -965,3 +1071,4 @@ const abacusaiPlugin = {
965
1071
  };
966
1072
 
967
1073
  export default abacusaiPlugin;
1074
+
@@ -1,5 +1,5 @@
1
1
  {
2
- "id": "abacusai-auth",
2
+ "id": "openclaw-abacusai-auth",
3
3
  "providers": [
4
4
  "abacusai"
5
5
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-abacusai-auth",
3
- "version": "1.2.4",
3
+ "version": "1.2.6",
4
4
  "description": "OpenClaw AbacusAI provider plugin - Third-party plugin for AbacusAI RouteLLM integration",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -18,6 +18,9 @@
18
18
  ],
19
19
  "author": "tonyhu2006",
20
20
  "license": "MIT",
21
+ "scripts": {
22
+ "build": "tsc"
23
+ },
21
24
  "peerDependencies": {
22
25
  "openclaw": ">=2026.2.0"
23
26
  },
@@ -25,5 +28,9 @@
25
28
  "extensions": [
26
29
  "./index.ts"
27
30
  ]
31
+ },
32
+ "devDependencies": {
33
+ "@types/node": "^25.3.3",
34
+ "typescript": "^5.9.3"
28
35
  }
29
36
  }
package/tsconfig.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "es2022",
4
+ "module": "nodenext",
5
+ "moduleResolution": "nodenext",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "skipLibCheck": true,
9
+ "forceConsistentCasingInFileNames": true,
10
+ "outDir": "./dist",
11
+ "declaration": true
12
+ },
13
+ "include": [
14
+ "index.ts"
15
+ ]
16
+ }