@t0ken.ai/memoryx-openclaw-plugin 2.2.63 → 2.2.65
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/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -1
- package/dist/hooks.d.ts +1 -1
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +2 -5
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +33 -31
- package/dist/proxy-credentials.d.ts +16 -41
- package/dist/proxy-credentials.d.ts.map +1 -1
- package/dist/proxy-credentials.js +33 -108
- package/dist/proxy-redirect.d.ts.map +1 -1
- package/dist/proxy-redirect.js +29 -22
- package/dist/sidecar.d.ts +3 -6
- package/dist/sidecar.d.ts.map +1 -1
- package/dist/sidecar.js +42 -120
- package/dist/vendor-normalize.d.ts +5 -5
- package/dist/vendor-normalize.d.ts.map +1 -1
- package/dist/vendor-normalize.js +7 -12
- package/package.json +1 -1
package/dist/constants.d.ts
CHANGED
package/dist/constants.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import * as path from "path";
|
|
5
5
|
import * as os from "os";
|
|
6
6
|
// Plugin version - synced from package.json by prebuild script
|
|
7
|
-
export const PLUGIN_VERSION = "2.2.
|
|
7
|
+
export const PLUGIN_VERSION = "2.2.65";
|
|
8
8
|
export const DEFAULT_API_BASE = "https://t0ken.ai/api";
|
|
9
9
|
export const PLUGIN_DIR = path.join(os.homedir(), ".openclaw", "extensions", "memoryx-openclaw-plugin");
|
|
10
10
|
/** 真实上游 baseUrl 缓存文件:重启后若配置已被改成 localhost,从此文件恢复各厂商真实地址。 */
|
package/dist/hooks.d.ts
CHANGED
|
@@ -4,5 +4,5 @@
|
|
|
4
4
|
import type { MemoryXPlugin } from "./plugin-core.js";
|
|
5
5
|
export declare function registerHooks(api: any, plugin: MemoryXPlugin, wrapProvidersWithProxy: () => void, applySidecarRedirect: (opts?: {
|
|
6
6
|
quiet?: boolean;
|
|
7
|
-
}) => void, shouldApplyProxy: () => boolean
|
|
7
|
+
}) => void, shouldApplyProxy: () => boolean): void;
|
|
8
8
|
//# sourceMappingURL=hooks.d.ts.map
|
package/dist/hooks.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../src/hooks.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEtD,wBAAgB,aAAa,CACzB,GAAG,EAAE,GAAG,EACR,MAAM,EAAE,aAAa,EACrB,sBAAsB,EAAE,MAAM,IAAI,EAClC,oBAAoB,EAAE,CAAC,IAAI,CAAC,EAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,KAAK,IAAI,EAC1D,gBAAgB,EAAE,MAAM,OAAO,
|
|
1
|
+
{"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../src/hooks.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEtD,wBAAgB,aAAa,CACzB,GAAG,EAAE,GAAG,EACR,MAAM,EAAE,aAAa,EACrB,sBAAsB,EAAE,MAAM,IAAI,EAClC,oBAAoB,EAAE,CAAC,IAAI,CAAC,EAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,KAAK,IAAI,EAC1D,gBAAgB,EAAE,MAAM,OAAO,GAChC,IAAI,CAoCN"}
|
package/dist/hooks.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export function registerHooks(api, plugin, wrapProvidersWithProxy, applySidecarRedirect, shouldApplyProxy
|
|
1
|
+
export function registerHooks(api, plugin, wrapProvidersWithProxy, applySidecarRedirect, shouldApplyProxy) {
|
|
2
2
|
let useVirtualProviderInLastRun = false;
|
|
3
3
|
api.on("message_received", async (event) => {
|
|
4
4
|
const { content } = event;
|
|
@@ -16,16 +16,13 @@ export function registerHooks(api, plugin, wrapProvidersWithProxy, applySidecarR
|
|
|
16
16
|
await plugin.onMessage("assistant", fullContent);
|
|
17
17
|
}
|
|
18
18
|
});
|
|
19
|
-
api.on("before_agent_start", async (
|
|
19
|
+
api.on("before_agent_start", async (_event) => {
|
|
20
20
|
if (shouldApplyProxy()) {
|
|
21
|
-
await syncRealUpstreamBaseUrlCache?.();
|
|
22
21
|
wrapProvidersWithProxy();
|
|
23
22
|
applySidecarRedirect({ quiet: true });
|
|
24
23
|
}
|
|
25
24
|
try {
|
|
26
25
|
await plugin.startTimersIfNeeded();
|
|
27
|
-
// 用户消息只由 message_received 写入一次,此处不再 onMessage("user", prompt),
|
|
28
|
-
// 避免与 message_received 重复,且 before_agent_start 可能被多次触发(如 tool 多轮)导致同条 prompt 重复入队。
|
|
29
26
|
}
|
|
30
27
|
catch (error) {
|
|
31
28
|
api.logger.warn(`[MemoryX] before_agent_start failed: ${error}`);
|
package/dist/index.d.ts
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
* - All initialization (DB, config loading, etc.) must be lazy-loaded
|
|
10
10
|
* - Use setImmediate() for deferred operations
|
|
11
11
|
*
|
|
12
|
+
* All file I/O (baseUrl cache) happens in service start (async).
|
|
13
|
+
*
|
|
12
14
|
* This version uses @t0ken.ai/memoryx-sdk with conversation preset:
|
|
13
15
|
* - maxTokens: 30000 (flush when reaching token limit)
|
|
14
16
|
* - intervalMs: 300000 (flush after 5 minutes idle)
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAG/C,OAAO,EAAE,aAAa,EAAU,MAAM,kBAAkB,CAAC;;;;;;kBAsBvC,GAAG,iBAAiB,YAAY,GAAG,IAAI;;AARzD,wBAmGE;AAEF,OAAO,EAAE,aAAa,EAAE,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
* - All initialization (DB, config loading, etc.) must be lazy-loaded
|
|
10
10
|
* - Use setImmediate() for deferred operations
|
|
11
11
|
*
|
|
12
|
+
* All file I/O (baseUrl cache) happens in service start (async).
|
|
13
|
+
*
|
|
12
14
|
* This version uses @t0ken.ai/memoryx-sdk with conversation preset:
|
|
13
15
|
* - maxTokens: 30000 (flush when reaching token limit)
|
|
14
16
|
* - intervalMs: 300000 (flush after 5 minutes idle)
|
|
@@ -37,50 +39,56 @@ export default {
|
|
|
37
39
|
}
|
|
38
40
|
plugin = new MemoryXPlugin(pluginConfig);
|
|
39
41
|
registerTools(api, plugin);
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
42
|
+
// Pure memory: read provider list from api.config (no file I/O, same as slimclaw)
|
|
43
|
+
const configCredentials = extractProviderCredentials(api.config);
|
|
44
|
+
log(`[Proxy] Found ${configCredentials.size} providers in config`);
|
|
45
|
+
const defaultConfig = getDefaultModelAndProvider(configCredentials, api.config);
|
|
43
46
|
log(`[Proxy] Default: ${defaultConfig.provider}/${defaultConfig.model}`);
|
|
44
47
|
const realProviderHeader = "X-MemoryX-Real-Provider";
|
|
45
48
|
const sidecarBaseUrl = `http://localhost:${SIDECAR_PORT}`;
|
|
46
|
-
const realUpstreamCredentials = realUpstreamCredentialsForSidecar(providerCredentials, new Map());
|
|
47
49
|
const proxyUrl = (pluginConfig?.apiBaseUrl || DEFAULT_API_BASE) + "/llm/proxy/chat/completions";
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
getSDK,
|
|
51
|
-
pluginConfig,
|
|
52
|
-
onStarted: (logFile) => api.logger.info(`[MemoryX] Plugin log file: ${logFile}`),
|
|
53
|
-
});
|
|
54
|
-
const getSidecarBase = () => `http://localhost:${sidecar.getPort()}`;
|
|
50
|
+
let sidecar = null;
|
|
51
|
+
const getSidecarBase = () => sidecar ? `http://localhost:${sidecar.getPort()}` : sidecarBaseUrl;
|
|
55
52
|
const { applySidecarRedirect, wrapProvidersWithProxy } = createProxyRedirect(api, getSidecarBase, realProviderHeader);
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
53
|
+
const shouldApplyProxy = () => {
|
|
54
|
+
const creds = extractProviderCredentials(api.config);
|
|
55
|
+
return getDefaultModelAndProvider(creds, api.config).provider === "memoryx-gateway";
|
|
56
|
+
};
|
|
57
|
+
registerHooks(api, plugin, wrapProvidersWithProxy, applySidecarRedirect, shouldApplyProxy);
|
|
60
58
|
api.registerService({
|
|
61
59
|
id: "memoryx-sidecar",
|
|
62
60
|
start: async () => {
|
|
63
61
|
try {
|
|
62
|
+
// Re-read credentials from api.config (pure memory, may have updated since register)
|
|
63
|
+
const credentials = extractProviderCredentials(api.config);
|
|
64
|
+
const defaults = getDefaultModelAndProvider(credentials, api.config);
|
|
65
|
+
// Sync baseUrl cache: config → compare with cache → update non-localhost
|
|
66
|
+
await syncRealUpstreamBaseUrlCacheFromConfig(api.config, getSidecarBase());
|
|
67
|
+
const realUpstreamCredentials = realUpstreamCredentialsForSidecar(credentials, new Map());
|
|
68
|
+
sidecar = new SidecarServer(realUpstreamCredentials, { model: defaults.model, provider: defaults.provider }, defaults.availableProviders, {
|
|
69
|
+
proxyUrl,
|
|
70
|
+
getSDK,
|
|
71
|
+
pluginConfig,
|
|
72
|
+
onStarted: (logFile) => api.logger.info(`[MemoryX] Plugin log file: ${logFile}`),
|
|
73
|
+
});
|
|
64
74
|
await sidecar.start();
|
|
65
75
|
api.logger.info(`[MemoryX] Sidecar started on port ${sidecar.getPort()}`);
|
|
76
|
+
if (defaults.provider === "memoryx-gateway") {
|
|
77
|
+
wrapProvidersWithProxy();
|
|
78
|
+
applySidecarRedirect();
|
|
79
|
+
}
|
|
66
80
|
}
|
|
67
81
|
catch (e) {
|
|
68
82
|
api.logger.error(`[MemoryX] Sidecar failed to start: ${e.message}`);
|
|
69
83
|
}
|
|
70
84
|
},
|
|
71
85
|
stop: async () => {
|
|
72
|
-
|
|
86
|
+
if (sidecar)
|
|
87
|
+
await sidecar.stop();
|
|
73
88
|
api.logger.info("[MemoryX] Sidecar stopped");
|
|
74
89
|
},
|
|
75
90
|
});
|
|
76
|
-
|
|
77
|
-
wrapProvidersWithProxy();
|
|
78
|
-
applySidecarRedirect();
|
|
79
|
-
api.logger.info(`[MemoryX] ✅ Plugin v${PLUGIN_VERSION} ready! Default is MemoryX Gateway, redirect applied.`);
|
|
80
|
-
}
|
|
81
|
-
else {
|
|
82
|
-
api.logger.info(`[MemoryX] ✅ Plugin v${PLUGIN_VERSION} ready! Default is ${defaultConfig.provider}/${defaultConfig.model}; switch to MemoryX Gateway to use MemoryX.`);
|
|
83
|
-
}
|
|
91
|
+
api.logger.info(`[MemoryX] ✅ Plugin v${PLUGIN_VERSION} registered (I/O deferred to service start).`);
|
|
84
92
|
setImmediate(async () => {
|
|
85
93
|
try {
|
|
86
94
|
await plugin.init();
|
|
@@ -89,13 +97,7 @@ export default {
|
|
|
89
97
|
api.logger.info(`[MemoryX] 💡 Manage your memories: https://t0ken.ai/portal?api_key=${info.apiKey}`);
|
|
90
98
|
}
|
|
91
99
|
}
|
|
92
|
-
catch (
|
|
93
|
-
// ignore
|
|
94
|
-
}
|
|
95
|
-
try {
|
|
96
|
-
await syncRealUpstreamBaseUrlCacheFromConfig(api.config, sidecarBaseUrl);
|
|
97
|
-
}
|
|
98
|
-
catch (e) {
|
|
100
|
+
catch (_) {
|
|
99
101
|
// ignore
|
|
100
102
|
}
|
|
101
103
|
});
|
|
@@ -1,67 +1,42 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Extract and resolve provider credentials from OpenClaw config
|
|
2
|
+
* Extract and resolve provider credentials from OpenClaw config (api.config).
|
|
3
3
|
*
|
|
4
|
-
* API key
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
4
|
+
* API key resolution order (same as slimclaw):
|
|
5
|
+
* 1. providerOverrides[id].apiKey (explicit)
|
|
6
|
+
* 2. api.config.models.providers[id].apiKey (openclaw.json)
|
|
7
|
+
* 3. providerOverrides[id].apiKeyEnv → process.env[name]
|
|
8
|
+
* 4. process.env[${PROVIDER_ID}_API_KEY]
|
|
9
|
+
*
|
|
10
|
+
* NO file I/O in this module's credential extraction.
|
|
11
|
+
* Only real-upstream-baseurl.json cache uses async file I/O.
|
|
11
12
|
*/
|
|
12
13
|
import type { ProviderCredentials } from "./types.js";
|
|
14
|
+
/**
|
|
15
|
+
* Extract provider credentials from api.config only.
|
|
16
|
+
* Follows slimclaw pattern: config.models.providers + env vars, no system file access.
|
|
17
|
+
*/
|
|
13
18
|
export declare function extractProviderCredentials(config: any, providerOverrides?: Record<string, {
|
|
14
19
|
baseUrl?: string;
|
|
15
20
|
apiKeyEnv?: string;
|
|
16
21
|
apiKey?: string;
|
|
17
22
|
}>): Map<string, ProviderCredentials>;
|
|
18
|
-
/**
|
|
19
|
-
* 在 init 时从 OpenClaw 配置(api.config)读到的「所有厂商 -> baseUrl」做一次快照,不写死任何厂商地址。
|
|
20
|
-
* - 来源唯一:插件启动时通过 api 读到的 openclaw 配置文件(openclaw.json 等)里的 models.providers[id].baseUrl。
|
|
21
|
-
* - 若某条已是 localhost(例如上次 redirect 被持久化),本次仍写入 localhost;恢复依赖 setImmediate 里从缓存文件合并(首次正常时已落盘)。
|
|
22
|
-
* 返回 Map<providerId, baseUrl>,memoryx-gateway 不写入。
|
|
23
|
-
*/
|
|
24
23
|
export declare function buildRealUpstreamBaseUrlMap(credentials: Map<string, ProviderCredentials>, _sidecarBaseUrl: string): Map<string, string>;
|
|
25
|
-
/**
|
|
26
|
-
* 用 init 时缓存的「真实上游 baseUrl 映射表」生成供 Sidecar 使用的 credentials。
|
|
27
|
-
* 发给 llm_proxy 的每个厂商的 targetUrl 均来自此映射表(初始化时的真实 baseUrl),不会被 redirect 后的 localhost 覆盖。
|
|
28
|
-
* 用户改配置后重启即可生效。
|
|
29
|
-
*/
|
|
30
24
|
export declare function realUpstreamCredentialsForSidecar(credentials: Map<string, ProviderCredentials>, realUpstreamBaseUrlMap: Map<string, string>): Map<string, ProviderCredentials>;
|
|
31
|
-
/**
|
|
32
|
-
* 从缓存文件加载「厂商 -> 真实 baseUrl」映射。文件不存在或解析失败返回空 Map。
|
|
33
|
-
* 供插件 sync 时使用(先读已有缓存,没有则当空)。
|
|
34
|
-
*/
|
|
35
25
|
export declare function loadRealUpstreamBaseUrlCache(): Promise<Map<string, string>>;
|
|
26
|
+
export declare function saveRealUpstreamBaseUrlCache(map: Map<string, string>): Promise<void>;
|
|
27
|
+
export declare function isLocalhostBaseUrl(url: string, sidecarBaseUrl: string): boolean;
|
|
36
28
|
/**
|
|
37
|
-
*
|
|
38
|
-
* 供 Sidecar 每次请求读盘使用,不缓存。
|
|
39
|
-
*/
|
|
40
|
-
export declare function loadRealUpstreamBaseUrlCacheStrict(): Promise<Map<string, string>>;
|
|
41
|
-
/**
|
|
42
|
-
* 根据 OpenClaw 配置更新 real-upstream-baseurl.json:非 localhost 的 baseUrl 写入/覆盖,localhost 绝不覆盖。
|
|
29
|
+
* Sync real-upstream-baseurl.json: config baseUrl vs cache, update non-localhost entries.
|
|
43
30
|
*/
|
|
44
31
|
export declare function syncRealUpstreamBaseUrlCacheFromConfig(config: any, sidecarBaseUrl: string): Promise<void>;
|
|
45
|
-
/**
|
|
46
|
-
* 将「厂商 -> 真实 baseUrl」映射写入缓存文件,重启后若配置被改成 localhost 可从此恢复。
|
|
47
|
-
*/
|
|
48
|
-
export declare function saveRealUpstreamBaseUrlCache(map: Map<string, string>): Promise<void>;
|
|
49
|
-
/** 判断是否为 localhost(或等于 sidecarBaseUrl),用于合并缓存时决定是否用缓存值覆盖。 */
|
|
50
|
-
export declare function isLocalhostBaseUrl(url: string, sidecarBaseUrl: string): boolean;
|
|
51
|
-
/** Get all available provider/model list (for server fallback/retry); only providers with apiKey set */
|
|
52
32
|
export declare function getAvailableProviders(credentials: Map<string, ProviderCredentials>): Array<{
|
|
53
33
|
provider: string;
|
|
54
34
|
model: string;
|
|
55
35
|
}>;
|
|
56
|
-
/**
|
|
57
|
-
* Default primary: one model per provider (first listed); only providers with apiKey set.
|
|
58
|
-
* Order follows config.models.providers key order (Map insertion order).
|
|
59
|
-
*/
|
|
60
36
|
export declare function getDefaultProviderModelList(credentials: Map<string, ProviderCredentials>): Array<{
|
|
61
37
|
provider: string;
|
|
62
38
|
model: string;
|
|
63
39
|
}>;
|
|
64
|
-
/** Get default model/provider and full availableProviders; prefer config.agents.defaults.model.primary, else first in list. */
|
|
65
40
|
export declare function getDefaultModelAndProvider(credentials: Map<string, ProviderCredentials>, config?: any): {
|
|
66
41
|
model: string;
|
|
67
42
|
provider: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"proxy-credentials.d.ts","sourceRoot":"","sources":["../src/proxy-credentials.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"proxy-credentials.d.ts","sourceRoot":"","sources":["../src/proxy-credentials.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAStD;;;GAGG;AACH,wBAAgB,0BAA0B,CACtC,MAAM,EAAE,GAAG,EACX,iBAAiB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,GAC9F,GAAG,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAuClC;AAMD,wBAAgB,2BAA2B,CACvC,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,mBAAmB,CAAC,EAC7C,eAAe,EAAE,MAAM,GACxB,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAQrB;AAED,wBAAgB,iCAAiC,CAC7C,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,mBAAmB,CAAC,EAC7C,sBAAsB,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAC5C,GAAG,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAWlC;AAED,wBAAsB,4BAA4B,IAAI,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAcjF;AAED,wBAAsB,4BAA4B,CAAC,GAAG,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAgB1F;AAED,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAI/E;AAED;;GAEG;AACH,wBAAsB,sCAAsC,CACxD,MAAM,EAAE,GAAG,EACX,cAAc,EAAE,MAAM,GACvB,OAAO,CAAC,IAAI,CAAC,CAiBf;AAMD,wBAAgB,qBAAqB,CACjC,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,mBAAmB,CAAC,GAC9C,KAAK,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAa5C;AAED,wBAAgB,2BAA2B,CACvC,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,mBAAmB,CAAC,GAC9C,KAAK,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAS5C;AAmBD,wBAAgB,0BAA0B,CACtC,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,mBAAmB,CAAC,EAC7C,MAAM,CAAC,EAAE,GAAG,GACb;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,kBAAkB,EAAE,KAAK,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CAAE,CA8BrG"}
|
|
@@ -1,40 +1,15 @@
|
|
|
1
1
|
import { log } from "./logger.js";
|
|
2
2
|
import { PLUGIN_DIR, REAL_UPSTREAM_BASEURL_CACHE_FILE } from "./constants.js";
|
|
3
3
|
import * as fs from "fs";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
if (!fs.existsSync(profilesPath))
|
|
12
|
-
return out;
|
|
13
|
-
const raw = fs.readFileSync(profilesPath, "utf8");
|
|
14
|
-
const data = JSON.parse(raw);
|
|
15
|
-
const profiles = data?.profiles ?? {};
|
|
16
|
-
for (const [profileId, p] of Object.entries(profiles)) {
|
|
17
|
-
if (!p || typeof p !== "object")
|
|
18
|
-
continue;
|
|
19
|
-
const isApiKey = (p.type === "api_key" || p.mode === "api_key");
|
|
20
|
-
const value = (p.key ?? p.token ?? "")?.trim();
|
|
21
|
-
if (!isApiKey || !value)
|
|
22
|
-
continue;
|
|
23
|
-
const providerId = profileId.includes(":") ? profileId.split(":")[0] : profileId;
|
|
24
|
-
if (!out.has(providerId))
|
|
25
|
-
out.set(providerId, value);
|
|
26
|
-
if (p.provider && p.provider !== providerId && !out.has(p.provider))
|
|
27
|
-
out.set(p.provider, value);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
catch (_) {
|
|
31
|
-
// ignore
|
|
32
|
-
}
|
|
33
|
-
return out;
|
|
34
|
-
}
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Provider credentials from api.config (pure memory, no file I/O)
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
/**
|
|
8
|
+
* Extract provider credentials from api.config only.
|
|
9
|
+
* Follows slimclaw pattern: config.models.providers + env vars, no system file access.
|
|
10
|
+
*/
|
|
35
11
|
export function extractProviderCredentials(config, providerOverrides) {
|
|
36
12
|
const credentials = new Map();
|
|
37
|
-
const authKeys = loadAuthProfilesKeys(DEFAULT_AUTH_PROFILES_PATH);
|
|
38
13
|
if (config?.models?.providers) {
|
|
39
14
|
for (const [id, providerConfig] of Object.entries(config.models.providers)) {
|
|
40
15
|
if (typeof providerConfig !== "object" ||
|
|
@@ -51,7 +26,6 @@ export function extractProviderCredentials(config, providerOverrides) {
|
|
|
51
26
|
(typeof pc.apiKey === "string" ? pc.apiKey.trim() : "") ||
|
|
52
27
|
(customEnvName ? process.env[customEnvName] : undefined) ||
|
|
53
28
|
process.env[`${id.toUpperCase()}_API_KEY`] ||
|
|
54
|
-
authKeys.get(id) ||
|
|
55
29
|
"";
|
|
56
30
|
const isMemoryxGateway = (id === "memoryx-gateway");
|
|
57
31
|
credentials.set(id, {
|
|
@@ -63,21 +37,13 @@ export function extractProviderCredentials(config, providerOverrides) {
|
|
|
63
37
|
}
|
|
64
38
|
}
|
|
65
39
|
if (credentials.size > 0) {
|
|
66
|
-
|
|
67
|
-
const authFromProfiles = [...credentials.keys()].filter((id) => authKeys.has(id));
|
|
68
|
-
const summary = authFromProfiles.length > 0
|
|
69
|
-
? `[Proxy] Credentials: ${parts.join(", ")}; apiKey from auth-profiles: ${authFromProfiles.join(", ")}`
|
|
70
|
-
: `[Proxy] Credentials: ${parts.join(", ")}`;
|
|
71
|
-
log(summary);
|
|
40
|
+
log(`[Proxy] Providers: ${[...credentials.keys()].join(", ")}`);
|
|
72
41
|
}
|
|
73
42
|
return credentials;
|
|
74
43
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
* - 若某条已是 localhost(例如上次 redirect 被持久化),本次仍写入 localhost;恢复依赖 setImmediate 里从缓存文件合并(首次正常时已落盘)。
|
|
79
|
-
* 返回 Map<providerId, baseUrl>,memoryx-gateway 不写入。
|
|
80
|
-
*/
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// baseUrl cache (real-upstream-baseurl.json) — async file I/O only
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
81
47
|
export function buildRealUpstreamBaseUrlMap(credentials, _sidecarBaseUrl) {
|
|
82
48
|
const map = new Map();
|
|
83
49
|
for (const [id, creds] of credentials) {
|
|
@@ -88,11 +54,6 @@ export function buildRealUpstreamBaseUrlMap(credentials, _sidecarBaseUrl) {
|
|
|
88
54
|
}
|
|
89
55
|
return map;
|
|
90
56
|
}
|
|
91
|
-
/**
|
|
92
|
-
* 用 init 时缓存的「真实上游 baseUrl 映射表」生成供 Sidecar 使用的 credentials。
|
|
93
|
-
* 发给 llm_proxy 的每个厂商的 targetUrl 均来自此映射表(初始化时的真实 baseUrl),不会被 redirect 后的 localhost 覆盖。
|
|
94
|
-
* 用户改配置后重启即可生效。
|
|
95
|
-
*/
|
|
96
57
|
export function realUpstreamCredentialsForSidecar(credentials, realUpstreamBaseUrlMap) {
|
|
97
58
|
const out = new Map();
|
|
98
59
|
for (const [id, creds] of credentials) {
|
|
@@ -105,10 +66,6 @@ export function realUpstreamCredentialsForSidecar(credentials, realUpstreamBaseU
|
|
|
105
66
|
}
|
|
106
67
|
return out;
|
|
107
68
|
}
|
|
108
|
-
/**
|
|
109
|
-
* 从缓存文件加载「厂商 -> 真实 baseUrl」映射。文件不存在或解析失败返回空 Map。
|
|
110
|
-
* 供插件 sync 时使用(先读已有缓存,没有则当空)。
|
|
111
|
-
*/
|
|
112
69
|
export async function loadRealUpstreamBaseUrlCache() {
|
|
113
70
|
try {
|
|
114
71
|
const raw = await fs.promises.readFile(REAL_UPSTREAM_BASEURL_CACHE_FILE, "utf8");
|
|
@@ -126,24 +83,28 @@ export async function loadRealUpstreamBaseUrlCache() {
|
|
|
126
83
|
return new Map();
|
|
127
84
|
}
|
|
128
85
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const map = new Map();
|
|
137
|
-
if (obj && typeof obj === "object") {
|
|
138
|
-
for (const [id, url] of Object.entries(obj)) {
|
|
139
|
-
if (id && typeof url === "string" && url.trim())
|
|
140
|
-
map.set(id, url.trim());
|
|
86
|
+
export async function saveRealUpstreamBaseUrlCache(map) {
|
|
87
|
+
try {
|
|
88
|
+
await fs.promises.mkdir(PLUGIN_DIR, { recursive: true });
|
|
89
|
+
const obj = {};
|
|
90
|
+
for (const [id, url] of map) {
|
|
91
|
+
if (id !== "memoryx-gateway" && url)
|
|
92
|
+
obj[id] = url;
|
|
141
93
|
}
|
|
94
|
+
await fs.promises.writeFile(REAL_UPSTREAM_BASEURL_CACHE_FILE, JSON.stringify(obj, null, 2), "utf8");
|
|
95
|
+
log(`[Proxy] baseUrl cache saved`);
|
|
96
|
+
}
|
|
97
|
+
catch (e) {
|
|
98
|
+
log(`[Proxy] Failed to save real upstream cache: ${e?.message ?? e}`);
|
|
142
99
|
}
|
|
143
|
-
|
|
100
|
+
}
|
|
101
|
+
export function isLocalhostBaseUrl(url, sidecarBaseUrl) {
|
|
102
|
+
const u = (url || "").trim().toLowerCase().replace(/\/$/, "");
|
|
103
|
+
const sidecarNorm = (sidecarBaseUrl || "").toLowerCase().replace(/\/$/, "");
|
|
104
|
+
return !u || u.includes("localhost") || (!!sidecarNorm && u === sidecarNorm);
|
|
144
105
|
}
|
|
145
106
|
/**
|
|
146
|
-
*
|
|
107
|
+
* Sync real-upstream-baseurl.json: config baseUrl vs cache, update non-localhost entries.
|
|
147
108
|
*/
|
|
148
109
|
export async function syncRealUpstreamBaseUrlCacheFromConfig(config, sidecarBaseUrl) {
|
|
149
110
|
let cached;
|
|
@@ -165,40 +126,12 @@ export async function syncRealUpstreamBaseUrlCacheFromConfig(config, sidecarBase
|
|
|
165
126
|
}
|
|
166
127
|
await saveRealUpstreamBaseUrlCache(merged);
|
|
167
128
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
export async function saveRealUpstreamBaseUrlCache(map) {
|
|
172
|
-
try {
|
|
173
|
-
await fs.promises.mkdir(PLUGIN_DIR, { recursive: true });
|
|
174
|
-
const obj = {};
|
|
175
|
-
for (const [id, url] of map) {
|
|
176
|
-
if (id !== "memoryx-gateway" && url)
|
|
177
|
-
obj[id] = url;
|
|
178
|
-
}
|
|
179
|
-
await fs.promises.writeFile(REAL_UPSTREAM_BASEURL_CACHE_FILE, JSON.stringify(obj, null, 2), "utf8");
|
|
180
|
-
log(`[Proxy] Saved real upstream baseUrl cache to ${REAL_UPSTREAM_BASEURL_CACHE_FILE}`);
|
|
181
|
-
}
|
|
182
|
-
catch (e) {
|
|
183
|
-
log(`[Proxy] Failed to save real upstream cache: ${e?.message ?? e}`);
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
/** 判断是否为 localhost(或等于 sidecarBaseUrl),用于合并缓存时决定是否用缓存值覆盖。 */
|
|
187
|
-
export function isLocalhostBaseUrl(url, sidecarBaseUrl) {
|
|
188
|
-
const u = (url || "").trim().toLowerCase().replace(/\/$/, "");
|
|
189
|
-
const sidecarNorm = (sidecarBaseUrl || "").toLowerCase().replace(/\/$/, "");
|
|
190
|
-
return !u || u.includes("localhost") || (!!sidecarNorm && u === sidecarNorm);
|
|
191
|
-
}
|
|
192
|
-
/** Only include providers with non-empty apiKey to avoid invalid Bearer header */
|
|
193
|
-
function hasApiKey(creds) {
|
|
194
|
-
return ((creds.apiKey ?? "").trim().length > 0);
|
|
195
|
-
}
|
|
196
|
-
/** Get all available provider/model list (for server fallback/retry); only providers with apiKey set */
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
// Provider list helpers (pure memory)
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
197
132
|
export function getAvailableProviders(credentials) {
|
|
198
133
|
const result = [];
|
|
199
134
|
for (const [providerId, creds] of credentials) {
|
|
200
|
-
if (!hasApiKey(creds))
|
|
201
|
-
continue;
|
|
202
135
|
if (creds.models && creds.models.length > 0) {
|
|
203
136
|
for (const m of creds.models) {
|
|
204
137
|
const id = m.id || m.name;
|
|
@@ -206,21 +139,15 @@ export function getAvailableProviders(credentials) {
|
|
|
206
139
|
result.push({ provider: providerId, model: id });
|
|
207
140
|
}
|
|
208
141
|
}
|
|
209
|
-
else {
|
|
142
|
+
else if (creds.baseUrl) {
|
|
210
143
|
result.push({ provider: providerId, model: "" });
|
|
211
144
|
}
|
|
212
145
|
}
|
|
213
146
|
return result;
|
|
214
147
|
}
|
|
215
|
-
/**
|
|
216
|
-
* Default primary: one model per provider (first listed); only providers with apiKey set.
|
|
217
|
-
* Order follows config.models.providers key order (Map insertion order).
|
|
218
|
-
*/
|
|
219
148
|
export function getDefaultProviderModelList(credentials) {
|
|
220
149
|
const result = [];
|
|
221
150
|
for (const [providerId, creds] of credentials) {
|
|
222
|
-
if (!hasApiKey(creds))
|
|
223
|
-
continue;
|
|
224
151
|
if (creds.models?.length) {
|
|
225
152
|
const id = creds.models[0].id || creds.models[0].name;
|
|
226
153
|
if (id)
|
|
@@ -229,7 +156,6 @@ export function getDefaultProviderModelList(credentials) {
|
|
|
229
156
|
}
|
|
230
157
|
return result;
|
|
231
158
|
}
|
|
232
|
-
/** Find provider/model in available list matching primary (primary format: "provider/modelId") */
|
|
233
159
|
function resolvePrimaryInAvailable(primary, available) {
|
|
234
160
|
if (!primary || typeof primary !== "string")
|
|
235
161
|
return null;
|
|
@@ -247,7 +173,6 @@ function resolvePrimaryInAvailable(primary, available) {
|
|
|
247
173
|
return { provider: wantProvider, model: wantModel };
|
|
248
174
|
return null;
|
|
249
175
|
}
|
|
250
|
-
/** Get default model/provider and full availableProviders; prefer config.agents.defaults.model.primary, else first in list. */
|
|
251
176
|
export function getDefaultModelAndProvider(credentials, config) {
|
|
252
177
|
const defaultList = getDefaultProviderModelList(credentials);
|
|
253
178
|
const availableAll = getAvailableProviders(credentials);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"proxy-redirect.d.ts","sourceRoot":"","sources":["../src/proxy-redirect.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"proxy-redirect.d.ts","sourceRoot":"","sources":["../src/proxy-redirect.ts"],"names":[],"mappings":"AAyBA,wBAAgB,mBAAmB,CAC/B,GAAG,EAAE,GAAG,EACR,cAAc,EAAE,MAAM,MAAM,EAC5B,kBAAkB,EAAE,MAAM,GAC3B;IACC,oBAAoB,EAAE,CAAC,IAAI,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,IAAI,CAAC;IAC3D,sBAAsB,EAAE,MAAM,IAAI,CAAC;CACtC,CAyDA"}
|
package/dist/proxy-redirect.js
CHANGED
|
@@ -1,22 +1,40 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Redirect provider baseUrl to Sidecar and wrap config so reads always resolve to Sidecar.
|
|
3
|
+
*
|
|
4
|
+
* Neither function performs file I/O. They only mutate api.config in memory.
|
|
3
5
|
*/
|
|
4
|
-
import { extractProviderCredentials } from "./proxy-credentials.js";
|
|
5
6
|
import { log } from "./logger.js";
|
|
7
|
+
/**
|
|
8
|
+
* Get all non-memoryx-gateway provider IDs from api.config.models.providers.
|
|
9
|
+
* Pure memory read, no file I/O.
|
|
10
|
+
*/
|
|
11
|
+
function getProviderIds(config) {
|
|
12
|
+
const providers = config?.models?.providers;
|
|
13
|
+
if (!providers || typeof providers !== "object")
|
|
14
|
+
return [];
|
|
15
|
+
const ids = [];
|
|
16
|
+
for (const id of Object.keys(providers)) {
|
|
17
|
+
if (id === "memoryx-gateway")
|
|
18
|
+
continue;
|
|
19
|
+
const p = providers[id];
|
|
20
|
+
if (p && typeof p === "object" && "baseUrl" in p) {
|
|
21
|
+
ids.push(id);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return ids;
|
|
25
|
+
}
|
|
6
26
|
export function createProxyRedirect(api, getSidecarBase, realProviderHeader) {
|
|
7
27
|
const applySidecarRedirect = (opts) => {
|
|
8
28
|
const sidecarBase = getSidecarBase();
|
|
9
|
-
const
|
|
10
|
-
if (
|
|
29
|
+
const providerIds = getProviderIds(api.config);
|
|
30
|
+
if (providerIds.length === 0) {
|
|
11
31
|
if (!opts?.quiet)
|
|
12
|
-
log(`[Proxy] No providers to redirect
|
|
32
|
+
log(`[Proxy] No providers to redirect`, { console: true });
|
|
13
33
|
return;
|
|
14
34
|
}
|
|
15
35
|
const providers = api.config.models.providers;
|
|
16
36
|
let n = 0;
|
|
17
|
-
for (const providerId of
|
|
18
|
-
if (providerId === "memoryx-gateway")
|
|
19
|
-
continue;
|
|
37
|
+
for (const providerId of providerIds) {
|
|
20
38
|
const p = providers[providerId];
|
|
21
39
|
if (!p || typeof p !== "object")
|
|
22
40
|
continue;
|
|
@@ -28,31 +46,20 @@ export function createProxyRedirect(api, getSidecarBase, realProviderHeader) {
|
|
|
28
46
|
log(`[Proxy] Redirected provider "${providerId}" baseUrl → Sidecar, header ${realProviderHeader}=${providerId}`);
|
|
29
47
|
}
|
|
30
48
|
if (n > 0 && !opts?.quiet) {
|
|
31
|
-
api.logger.info(`[MemoryX] ✅ Sidecar redirect applied for ${n} provider(s)
|
|
32
|
-
log(`[Proxy] Sidecar redirect applied for ${n} provider(s)`, { console: true });
|
|
33
|
-
}
|
|
34
|
-
if (n > 0) {
|
|
35
|
-
for (const providerId of creds.keys()) {
|
|
36
|
-
if (providerId === "memoryx-gateway")
|
|
37
|
-
continue;
|
|
38
|
-
const p = providers[providerId];
|
|
39
|
-
const readBack = (p && typeof p === "object" && p.baseUrl) ? p.baseUrl : "<missing>";
|
|
40
|
-
const isSidecar = readBack === sidecarBase;
|
|
41
|
-
log(`[Proxy] Readback ${providerId}.baseUrl = ${readBack} ${isSidecar ? "(Sidecar ✓)" : "(NOT Sidecar!)"}`);
|
|
42
|
-
}
|
|
49
|
+
api.logger.info(`[MemoryX] ✅ Sidecar redirect applied for ${n} provider(s).`);
|
|
43
50
|
}
|
|
44
51
|
};
|
|
45
52
|
const wrapProvidersWithProxy = () => {
|
|
46
53
|
if (!api.config?.models?.providers || typeof api.config.models.providers !== "object")
|
|
47
54
|
return;
|
|
48
|
-
const
|
|
49
|
-
if (
|
|
55
|
+
const providerIds = new Set(getProviderIds(api.config));
|
|
56
|
+
if (providerIds.size === 0)
|
|
50
57
|
return;
|
|
51
58
|
const raw = api.config.models.providers;
|
|
52
59
|
const handler = {
|
|
53
60
|
get(target, prop) {
|
|
54
61
|
const val = target[prop];
|
|
55
|
-
if (val && typeof val === "object" &&
|
|
62
|
+
if (val && typeof val === "object" && providerIds.has(prop)) {
|
|
56
63
|
return new Proxy(val, {
|
|
57
64
|
get(t, k) {
|
|
58
65
|
if (k === "baseUrl")
|
package/dist/sidecar.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Local HTTP Sidecar:
|
|
2
|
+
* Local HTTP Sidecar: lightweight forwarder.
|
|
3
|
+
* Reads base_url_map from local cache file, credentials from constructor (api.config).
|
|
4
|
+
* Packages payload and POSTs to MemoryX API server.
|
|
3
5
|
*/
|
|
4
6
|
import type { ProviderCredentials } from "./types.js";
|
|
5
7
|
import type { PluginConfig } from "./types.js";
|
|
@@ -26,11 +28,6 @@ export declare class SidecarServer {
|
|
|
26
28
|
start(): Promise<void>;
|
|
27
29
|
stop(): Promise<void>;
|
|
28
30
|
getPort(): number;
|
|
29
|
-
/** For logging: redact headers (Authorization, x-api-key, etc. show as ***) */
|
|
30
|
-
private redactHeaders;
|
|
31
|
-
/** Build forward request headers from apiKey in OpenClaw style; does not overwrite user config */
|
|
32
|
-
private buildForwardHeaders;
|
|
33
|
-
/** 仅转发到服务端 proxy(记忆注入),无本地回落。 */
|
|
34
31
|
private handleRequest;
|
|
35
32
|
private readBody;
|
|
36
33
|
}
|
package/dist/sidecar.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sidecar.d.ts","sourceRoot":"","sources":["../src/sidecar.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"sidecar.d.ts","sourceRoot":"","sources":["../src/sidecar.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AACtD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAK/C,MAAM,MAAM,MAAM,GAAG,CAAC,MAAM,CAAC,EAAE,YAAY,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC;AAE7D,MAAM,WAAW,cAAc;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CACzC;AAED,qBAAa,aAAa;IACtB,OAAO,CAAC,MAAM,CAA4B;IAC1C,OAAO,CAAC,WAAW,CAAmC;IACtD,OAAO,CAAC,eAAe,CAAsC;IAC7D,OAAO,CAAC,kBAAkB,CAA6C;IACvE,OAAO,CAAC,OAAO,CAAiB;gBAG5B,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,mBAAmB,CAAC,EAC7C,eAAe,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,EACpD,kBAAkB,EAAE,KAAK,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,EAC9D,OAAO,EAAE,cAAc;IAQrB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAwBtB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAa3B,OAAO,IAAI,MAAM;YAMH,aAAa;IA+K3B,OAAO,CAAC,QAAQ;CAQnB"}
|
package/dist/sidecar.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Local HTTP Sidecar:
|
|
2
|
+
* Local HTTP Sidecar: lightweight forwarder.
|
|
3
|
+
* Reads base_url_map from local cache file, credentials from constructor (api.config).
|
|
4
|
+
* Packages payload and POSTs to MemoryX API server.
|
|
3
5
|
*/
|
|
4
6
|
import * as http from "http";
|
|
5
7
|
import { SIDECAR_PORT } from "./constants.js";
|
|
6
8
|
import { log, LOG_FILE } from "./logger.js";
|
|
7
|
-
import {
|
|
9
|
+
import { loadRealUpstreamBaseUrlCache } from "./proxy-credentials.js";
|
|
8
10
|
export class SidecarServer {
|
|
9
11
|
server = null;
|
|
10
12
|
credentials;
|
|
@@ -59,32 +61,6 @@ export class SidecarServer {
|
|
|
59
61
|
? this.server.address().port
|
|
60
62
|
: SIDECAR_PORT;
|
|
61
63
|
}
|
|
62
|
-
/** For logging: redact headers (Authorization, x-api-key, etc. show as ***) */
|
|
63
|
-
redactHeaders(h) {
|
|
64
|
-
const out = {};
|
|
65
|
-
const secretKeys = ["authorization", "x-api-key", "cookie"];
|
|
66
|
-
for (const [k, v] of Object.entries(h)) {
|
|
67
|
-
const lower = k.toLowerCase();
|
|
68
|
-
out[k] = secretKeys.some((s) => lower === s || lower.includes("api-key")) ? "***" : v;
|
|
69
|
-
}
|
|
70
|
-
return out;
|
|
71
|
-
}
|
|
72
|
-
/** Build forward request headers from apiKey in OpenClaw style; does not overwrite user config */
|
|
73
|
-
buildForwardHeaders(provider, apiKey) {
|
|
74
|
-
const h = { "Content-Type": "application/json" };
|
|
75
|
-
const key = (apiKey || "").trim();
|
|
76
|
-
if (!key)
|
|
77
|
-
return h;
|
|
78
|
-
if (provider === "anthropic") {
|
|
79
|
-
h["x-api-key"] = key;
|
|
80
|
-
h["anthropic-version"] = "2023-06-01";
|
|
81
|
-
}
|
|
82
|
-
else {
|
|
83
|
-
h["Authorization"] = `Bearer ${key}`;
|
|
84
|
-
}
|
|
85
|
-
return h;
|
|
86
|
-
}
|
|
87
|
-
/** 仅转发到服务端 proxy(记忆注入),无本地回落。 */
|
|
88
64
|
async handleRequest(req, res) {
|
|
89
65
|
const url = req.url || "/";
|
|
90
66
|
const method = req.method?.toUpperCase();
|
|
@@ -95,6 +71,7 @@ export class SidecarServer {
|
|
|
95
71
|
return;
|
|
96
72
|
}
|
|
97
73
|
const reqId = `req-${Date.now()}`;
|
|
74
|
+
log(`[Sidecar] ${reqId} in ${method} ${url}`, { console: true });
|
|
98
75
|
let body;
|
|
99
76
|
try {
|
|
100
77
|
body = await this.readBody(req);
|
|
@@ -112,91 +89,37 @@ export class SidecarServer {
|
|
|
112
89
|
catch {
|
|
113
90
|
openaiRequest = {};
|
|
114
91
|
}
|
|
115
|
-
const messages = openaiRequest.messages || [];
|
|
116
|
-
const modelStr = openaiRequest.model || "";
|
|
117
92
|
const stream = !!openaiRequest.stream;
|
|
118
|
-
|
|
119
|
-
let provider = rawProvider || (modelStr.includes("/") ? modelStr.split("/")[0] : "") || "";
|
|
120
|
-
if (provider === "memoryx-gateway") {
|
|
121
|
-
const real = this.availableProviders.find((p) => p.provider !== "memoryx-gateway");
|
|
122
|
-
provider = real ? real.provider : "";
|
|
123
|
-
}
|
|
124
|
-
if (!provider || !this.credentials.has(provider)) {
|
|
125
|
-
provider = this.defaultProvider?.provider || "";
|
|
126
|
-
}
|
|
127
|
-
if (!provider || !this.credentials.has(provider)) {
|
|
128
|
-
for (const [id] of this.credentials) {
|
|
129
|
-
if (id !== "memoryx-gateway") {
|
|
130
|
-
provider = id;
|
|
131
|
-
break;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
let creds = provider ? this.credentials.get(provider) : undefined;
|
|
136
|
-
if (!creds || !(creds.apiKey ?? "").trim()) {
|
|
137
|
-
for (const [id, c] of this.credentials) {
|
|
138
|
-
if (id !== "memoryx-gateway" && (c.apiKey ?? "").trim()) {
|
|
139
|
-
provider = id;
|
|
140
|
-
creds = c;
|
|
141
|
-
break;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
let apiKey = (creds?.apiKey ?? "").trim();
|
|
146
|
-
if (!provider || !creds || !apiKey) {
|
|
147
|
-
res.writeHead(502, { "Content-Type": "application/json" });
|
|
148
|
-
res.end(JSON.stringify({ error: "No upstream (no apiKey for provider)" }));
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
if (provider === "memoryx-gateway") {
|
|
152
|
-
const real = this.availableProviders.find((p) => p.provider !== "memoryx-gateway");
|
|
153
|
-
if (!real) {
|
|
154
|
-
res.writeHead(502, { "Content-Type": "application/json" });
|
|
155
|
-
res.end(JSON.stringify({
|
|
156
|
-
error: "memoryx-gateway is virtual; no real LLM provider in config. Add a provider (e.g. zai, anthropic) in OpenClaw models.providers.",
|
|
157
|
-
}));
|
|
158
|
-
return;
|
|
159
|
-
}
|
|
160
|
-
provider = real.provider;
|
|
161
|
-
const realCreds = this.credentials.get(provider);
|
|
162
|
-
if (!realCreds || !(realCreds.apiKey ?? "").trim()) {
|
|
163
|
-
res.writeHead(502, { "Content-Type": "application/json" });
|
|
164
|
-
res.end(JSON.stringify({
|
|
165
|
-
error: `No apiKey for real provider '${provider}'. Configure it in OpenClaw (models.providers or auth-profiles).`,
|
|
166
|
-
}));
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
169
|
-
creds = realCreds;
|
|
170
|
-
apiKey = (creds.apiKey ?? "").trim();
|
|
171
|
-
}
|
|
93
|
+
// Read base_url_map from cache file (async)
|
|
172
94
|
let baseUrlMap;
|
|
173
95
|
try {
|
|
174
|
-
baseUrlMap = await
|
|
96
|
+
baseUrlMap = await loadRealUpstreamBaseUrlCache();
|
|
175
97
|
}
|
|
176
|
-
catch
|
|
177
|
-
|
|
178
|
-
res.end(JSON.stringify({
|
|
179
|
-
error: "real-upstream-baseurl.json could not be read: " + (e?.message ?? String(e)),
|
|
180
|
-
}));
|
|
181
|
-
return;
|
|
98
|
+
catch {
|
|
99
|
+
baseUrlMap = new Map();
|
|
182
100
|
}
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
error: `Missing real upstream baseUrl for provider '${provider}'. Ensure real-upstream-baseurl.json is populated (e.g. from OpenClaw config).`,
|
|
188
|
-
}));
|
|
189
|
-
return;
|
|
101
|
+
const baseUrlMapObj = {};
|
|
102
|
+
for (const [id, u] of baseUrlMap) {
|
|
103
|
+
if (id && u?.trim())
|
|
104
|
+
baseUrlMapObj[id] = u.trim();
|
|
190
105
|
}
|
|
191
|
-
const
|
|
192
|
-
const
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
106
|
+
const rawProvider = req.headers["x-memoryx-real-provider"]?.trim() || "";
|
|
107
|
+
const authHeader = req.headers["authorization"]?.trim() || "";
|
|
108
|
+
const incomingApiKey = authHeader.startsWith("Bearer ")
|
|
109
|
+
? authHeader.slice(7).trim()
|
|
110
|
+
: authHeader;
|
|
111
|
+
// Build credentials: use incoming Authorization header as apiKey for the target provider
|
|
112
|
+
const credentialsObj = {};
|
|
113
|
+
for (const [id, creds] of this.credentials) {
|
|
114
|
+
const realBaseUrl = baseUrlMapObj[id] || creds.baseUrl || "";
|
|
115
|
+
const apiKey = (id === rawProvider && incomingApiKey)
|
|
116
|
+
? incomingApiKey
|
|
117
|
+
: (creds.apiKey || "");
|
|
118
|
+
credentialsObj[id] = { baseUrl: realBaseUrl, apiKey };
|
|
199
119
|
}
|
|
120
|
+
const messages = openaiRequest.messages || [];
|
|
121
|
+
const lastUserMsg = [...messages].reverse().find((m) => m.role === "user");
|
|
122
|
+
const searchQuery = typeof lastUserMsg?.content === "string" ? lastUserMsg.content : "";
|
|
200
123
|
let memoryxApiKey = "";
|
|
201
124
|
let agentId = "";
|
|
202
125
|
try {
|
|
@@ -206,21 +129,20 @@ export class SidecarServer {
|
|
|
206
129
|
agentId = accountInfo.agentId || "";
|
|
207
130
|
}
|
|
208
131
|
catch (_e) {
|
|
209
|
-
/*
|
|
132
|
+
/* do not block */
|
|
210
133
|
}
|
|
211
|
-
const headers = this.buildForwardHeaders(provider, apiKey);
|
|
212
|
-
const lastUserMsg = [...messages].reverse().find((m) => m.role === "user");
|
|
213
|
-
const searchQuery = typeof lastUserMsg?.content === "string" ? lastUserMsg.content : "";
|
|
214
134
|
const proxyRequestBody = {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
135
|
+
base_url_map: baseUrlMapObj,
|
|
136
|
+
credentials: credentialsObj,
|
|
137
|
+
incoming_api_key: incomingApiKey || undefined,
|
|
218
138
|
body: openaiRequest,
|
|
219
|
-
|
|
139
|
+
request_headers: { "x-memoryx-real-provider": rawProvider },
|
|
140
|
+
search_query: searchQuery,
|
|
220
141
|
agent_id: agentId,
|
|
221
142
|
available_models: this.availableProviders.map((p) => `${p.provider}/${p.model}`),
|
|
222
143
|
};
|
|
223
|
-
|
|
144
|
+
const pathLog = (req.url || "/").split("?")[0] || "/";
|
|
145
|
+
log(`[Sidecar] ${reqId} ${method} ${pathLog} model=${openaiRequest.model ?? "-"} stream=${stream} → proxy ${proxyUrl}`, { console: true });
|
|
224
146
|
let proxyResponse = null;
|
|
225
147
|
try {
|
|
226
148
|
proxyResponse = await fetch(proxyUrl, {
|
|
@@ -245,7 +167,7 @@ export class SidecarServer {
|
|
|
245
167
|
}
|
|
246
168
|
}
|
|
247
169
|
catch (_) {
|
|
248
|
-
/* drain
|
|
170
|
+
/* drain */
|
|
249
171
|
}
|
|
250
172
|
}
|
|
251
173
|
const status = proxyResponse?.status ?? 502;
|
|
@@ -262,7 +184,7 @@ export class SidecarServer {
|
|
|
262
184
|
try {
|
|
263
185
|
firstChunk = await Promise.race([
|
|
264
186
|
reader.read(),
|
|
265
|
-
new Promise((_, rej) => setTimeout(() => rej(new Error("First chunk timeout")),
|
|
187
|
+
new Promise((_, rej) => setTimeout(() => rej(new Error("First chunk timeout")), 60000)),
|
|
266
188
|
]);
|
|
267
189
|
}
|
|
268
190
|
catch (firstErr) {
|
|
@@ -289,7 +211,7 @@ export class SidecarServer {
|
|
|
289
211
|
reader.releaseLock();
|
|
290
212
|
}
|
|
291
213
|
res.end();
|
|
292
|
-
log(`[Sidecar] ${reqId} ← ${proxy.status} stream
|
|
214
|
+
log(`[Sidecar] ${reqId} ← ${proxy.status} stream`, { console: true });
|
|
293
215
|
return;
|
|
294
216
|
}
|
|
295
217
|
if (!proxy.body) {
|
|
@@ -297,11 +219,11 @@ export class SidecarServer {
|
|
|
297
219
|
"Content-Type": proxy.headers.get("content-type") || "application/json",
|
|
298
220
|
});
|
|
299
221
|
res.end();
|
|
300
|
-
log(`[Sidecar] ${reqId} ← ${proxy.status} (no body)
|
|
222
|
+
log(`[Sidecar] ${reqId} ← ${proxy.status} (no body)`, { console: true });
|
|
301
223
|
return;
|
|
302
224
|
}
|
|
303
225
|
const text = await proxy.text();
|
|
304
|
-
log(`[Sidecar] ${reqId} ← ${proxy.status} body=${text.length}
|
|
226
|
+
log(`[Sidecar] ${reqId} ← ${proxy.status} body=${text.length}`, { console: true });
|
|
305
227
|
res.writeHead(proxy.status, {
|
|
306
228
|
"Content-Type": proxy.headers.get("content-type") || "application/json",
|
|
307
229
|
});
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 与服务端 llm_proxy 一致的厂商请求体规范化。
|
|
3
|
-
*
|
|
3
|
+
* 当前插件内无调用方,规范化由服务端 llm_proxy 完成;保留本模块供将来如需在插件侧做兼容时复用。
|
|
4
4
|
*/
|
|
5
5
|
/**
|
|
6
|
-
* 判断是否为 Z.AI GLM-5:按 provider
|
|
6
|
+
* 判断是否为 Z.AI GLM-5:按 provider 与 model 判断(不再依赖 targetUrl)。
|
|
7
7
|
*/
|
|
8
|
-
export declare function isZaiGlm5(provider: string,
|
|
8
|
+
export declare function isZaiGlm5(provider: string, model: string): boolean;
|
|
9
9
|
/**
|
|
10
|
-
* 按厂商与
|
|
10
|
+
* 按厂商与 model 规范化请求体;与服务端逻辑一致。当前插件内无调用,规范化在服务端完成。
|
|
11
11
|
* 返回规范化后的 body(不修改入参)。
|
|
12
12
|
*/
|
|
13
|
-
export declare function normalizeBodyForVendor(provider: string,
|
|
13
|
+
export declare function normalizeBodyForVendor(provider: string, body: Record<string, unknown>): Record<string, unknown>;
|
|
14
14
|
//# sourceMappingURL=vendor-normalize.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vendor-normalize.d.ts","sourceRoot":"","sources":["../src/vendor-normalize.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH;;GAEG;AACH,wBAAgB,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,
|
|
1
|
+
{"version":3,"file":"vendor-normalize.d.ts","sourceRoot":"","sources":["../src/vendor-normalize.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH;;GAEG;AACH,wBAAgB,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAIlE;AA4DD;;;GAGG;AACH,wBAAgB,sBAAsB,CAClC,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC9B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAMzB"}
|
package/dist/vendor-normalize.js
CHANGED
|
@@ -1,19 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 与服务端 llm_proxy 一致的厂商请求体规范化。
|
|
3
|
-
*
|
|
3
|
+
* 当前插件内无调用方,规范化由服务端 llm_proxy 完成;保留本模块供将来如需在插件侧做兼容时复用。
|
|
4
4
|
*/
|
|
5
5
|
/**
|
|
6
|
-
* 判断是否为 Z.AI GLM-5:按 provider
|
|
6
|
+
* 判断是否为 Z.AI GLM-5:按 provider 与 model 判断(不再依赖 targetUrl)。
|
|
7
7
|
*/
|
|
8
|
-
export function isZaiGlm5(provider,
|
|
8
|
+
export function isZaiGlm5(provider, model) {
|
|
9
9
|
const prov = (provider || "").toLowerCase();
|
|
10
|
-
const url = (targetUrl || "").toLowerCase();
|
|
11
10
|
const m = (model || "").toLowerCase();
|
|
12
|
-
|
|
13
|
-
return true;
|
|
14
|
-
if ((url.includes("open.bigmodel.cn") || url.includes("api.z.ai")) && (m === "glm-5" || m.endsWith("/glm-5")))
|
|
15
|
-
return true;
|
|
16
|
-
return false;
|
|
11
|
+
return prov === "zai" && (m === "glm-5" || m.endsWith("/glm-5"));
|
|
17
12
|
}
|
|
18
13
|
/**
|
|
19
14
|
* Z.AI GLM-5 兼容:与 api/app/routers/llm_proxy.py _normalize_body_for_zai_glm5 保持一致。
|
|
@@ -70,12 +65,12 @@ function normalizeBodyForZaiGlm5(body) {
|
|
|
70
65
|
return out;
|
|
71
66
|
}
|
|
72
67
|
/**
|
|
73
|
-
* 按厂商与
|
|
68
|
+
* 按厂商与 model 规范化请求体;与服务端逻辑一致。当前插件内无调用,规范化在服务端完成。
|
|
74
69
|
* 返回规范化后的 body(不修改入参)。
|
|
75
70
|
*/
|
|
76
|
-
export function normalizeBodyForVendor(provider,
|
|
71
|
+
export function normalizeBodyForVendor(provider, body) {
|
|
77
72
|
const model = String(body?.model || "").trim();
|
|
78
|
-
if (isZaiGlm5(provider,
|
|
73
|
+
if (isZaiGlm5(provider, model)) {
|
|
79
74
|
return normalizeBodyForZaiGlm5(body);
|
|
80
75
|
}
|
|
81
76
|
return body;
|
package/package.json
CHANGED