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 +123 -16
- package/openclaw.plugin.json +1 -1
- package/package.json +8 -1
- package/tsconfig.json +16 -0
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
|
-
//
|
|
19
|
-
|
|
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
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
proxyPort =
|
|
685
|
-
|
|
686
|
-
|
|
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
|
-
|
|
690
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openclaw-abacusai-auth",
|
|
3
|
-
"version": "1.2.
|
|
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
|
+
}
|