@t0ken.ai/memoryx-openclaw-plugin 2.2.61 → 2.2.63

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/README.md CHANGED
@@ -175,14 +175,14 @@ Memories are categorized by the server:
175
175
  **SlimClaw-style (recommended, no file changes):** Like SlimClaw, you can use a **virtual provider** so all traffic goes through the Sidecar without the plugin mutating config or files. Add this to `~/.openclaw/openclaw.json` under `models.providers`:
176
176
 
177
177
  ```json
178
- "memoryx-proxy": {
178
+ "memoryx-gateway": {
179
179
  "baseUrl": "http://localhost:3335/v1",
180
180
  "apiKey": "placeholder",
181
181
  "models": [{ "id": "auto" }]
182
182
  }
183
183
  ```
184
184
 
185
- Then add an auth profile for `memoryx-proxy` (e.g. in `~/.openclaw/agents/main/agent/auth-profiles.json` or global auth) with any placeholder key. In the UI, choose model **memoryx-proxy/auto**. Requests will go to the Sidecar, which forwards to the MemoryX server with your real provider credentials (you configure which provider/model to use via the plugin/Sidecar logic). The plugin does not modify your config or `models.json`; it only runs the Sidecar.
185
+ Then add an auth profile for `memoryx-gateway` (e.g. in `~/.openclaw/agents/main/agent/auth-profiles.json` or global auth) with any placeholder key. In the UI, choose model **MemoryX Gateway** (id: `memoryx-gateway/auto`). Requests will go to the Sidecar, which forwards to the MemoryX server with your real provider credentials (you configure which provider/model to use via the plugin/Sidecar logic). The plugin does not modify your config or `models.json`; it only runs the Sidecar.
186
186
 
187
187
  ## License
188
188
 
@@ -1,8 +1,10 @@
1
- export declare const PLUGIN_VERSION = "2.2.61";
1
+ export declare const PLUGIN_VERSION = "2.2.63";
2
2
  export declare const DEFAULT_API_BASE = "https://t0ken.ai/api";
3
3
  export declare const PLUGIN_DIR: string;
4
4
  /** 真实上游 baseUrl 缓存文件:重启后若配置已被改成 localhost,从此文件恢复各厂商真实地址。 */
5
5
  export declare const REAL_UPSTREAM_BASEURL_CACHE_FILE: string;
6
6
  /** Sidecar HTTP port — fixed so openclaw.json can point to localhost:37169. Do not use a range. */
7
7
  export declare const SIDECAR_PORT = 37169;
8
+ /** 单条 plugin.log 最大字节数,超过则轮转为 plugin.log.old 并重新写。 */
9
+ export declare const MAX_LOG_FILE_BYTES: number;
8
10
  //# sourceMappingURL=constants.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAOA,eAAO,MAAM,cAAc,WAAW,CAAC;AAEvC,eAAO,MAAM,gBAAgB,yBAAyB,CAAC;AAEvD,eAAO,MAAM,UAAU,QAAgF,CAAC;AAExG,4DAA4D;AAC5D,eAAO,MAAM,gCAAgC,QAAsD,CAAC;AAEpG,mGAAmG;AACnG,eAAO,MAAM,YAAY,QAAQ,CAAC"}
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAOA,eAAO,MAAM,cAAc,WAAW,CAAC;AAEvC,eAAO,MAAM,gBAAgB,yBAAyB,CAAC;AAEvD,eAAO,MAAM,UAAU,QAAgF,CAAC;AAExG,4DAA4D;AAC5D,eAAO,MAAM,gCAAgC,QAAsD,CAAC;AAEpG,mGAAmG;AACnG,eAAO,MAAM,YAAY,QAAQ,CAAC;AAElC,sDAAsD;AACtD,eAAO,MAAM,kBAAkB,QAAkB,CAAC"}
package/dist/constants.js CHANGED
@@ -4,10 +4,12 @@
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.61";
7
+ export const PLUGIN_VERSION = "2.2.63";
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,从此文件恢复各厂商真实地址。 */
11
11
  export const REAL_UPSTREAM_BASEURL_CACHE_FILE = path.join(PLUGIN_DIR, "real-upstream-baseurl.json");
12
12
  /** Sidecar HTTP port — fixed so openclaw.json can point to localhost:37169. Do not use a range. */
13
13
  export const SIDECAR_PORT = 37169;
14
+ /** 单条 plugin.log 最大字节数,超过则轮转为 plugin.log.old 并重新写。 */
15
+ export const MAX_LOG_FILE_BYTES = 2 * 1024 * 1024; // 2MB
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): void;
7
+ }) => void, shouldApplyProxy: () => boolean, syncRealUpstreamBaseUrlCache?: () => Promise<void>): void;
8
8
  //# sourceMappingURL=hooks.d.ts.map
@@ -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,GAC3D,IAAI,CAyCN"}
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,EAC/B,4BAA4B,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GACnD,IAAI,CAuCN"}
package/dist/hooks.js CHANGED
@@ -1,4 +1,4 @@
1
- export function registerHooks(api, plugin, wrapProvidersWithProxy, applySidecarRedirect) {
1
+ export function registerHooks(api, plugin, wrapProvidersWithProxy, applySidecarRedirect, shouldApplyProxy, syncRealUpstreamBaseUrlCache) {
2
2
  let useVirtualProviderInLastRun = false;
3
3
  api.on("message_received", async (event) => {
4
4
  const { content } = event;
@@ -6,27 +6,26 @@ export function registerHooks(api, plugin, wrapProvidersWithProxy, applySidecarR
6
6
  await plugin.onMessage("user", content);
7
7
  });
8
8
  api.on("llm_input", (_event) => {
9
- useVirtualProviderInLastRun = _event?.provider === "memoryx-proxy";
9
+ useVirtualProviderInLastRun = _event?.provider === "memoryx-gateway";
10
10
  });
11
11
  api.on("llm_output", async (event) => {
12
- const { assistantTexts, provider } = event;
13
- if (assistantTexts && Array.isArray(assistantTexts) && plugin && provider !== "memoryx-proxy") {
12
+ const { assistantTexts } = event;
13
+ if (assistantTexts && Array.isArray(assistantTexts) && plugin) {
14
14
  const fullContent = assistantTexts.join("\n");
15
15
  if (fullContent && fullContent.length >= 2)
16
16
  await plugin.onMessage("assistant", fullContent);
17
17
  }
18
18
  });
19
19
  api.on("before_agent_start", async (event) => {
20
- wrapProvidersWithProxy();
21
- applySidecarRedirect({ quiet: true });
22
- const { prompt } = event;
23
- if (!prompt || prompt.length < 2 || !plugin)
24
- return;
20
+ if (shouldApplyProxy()) {
21
+ await syncRealUpstreamBaseUrlCache?.();
22
+ wrapProvidersWithProxy();
23
+ applySidecarRedirect({ quiet: true });
24
+ }
25
25
  try {
26
26
  await plugin.startTimersIfNeeded();
27
- await plugin.onMessage("user", prompt);
28
- // 记忆注入改由服务端 llm_proxy 在转发前注入到请求体(system/消息),不在此处用 prependContext,
29
- // 避免 OpenClaw 把 prependContext 当成单独用户消息写入历史(role=user 的 MemoryX Context 条)。
27
+ // 用户消息只由 message_received 写入一次,此处不再 onMessage("user", prompt)
28
+ // 避免与 message_received 重复,且 before_agent_start 可能被多次触发(如 tool 多轮)导致同条 prompt 重复入队。
30
29
  }
31
30
  catch (error) {
32
31
  api.logger.warn(`[MemoryX] before_agent_start failed: ${error}`);
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAG/C,OAAO,EAAE,aAAa,EAAU,MAAM,kBAAkB,CAAC;;;;;;kBAyBvC,GAAG,iBAAiB,YAAY,GAAG,IAAI;;AARzD,wBAkGE;AAEF,OAAO,EAAE,aAAa,EAAE,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;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,wBA6FE;AAEF,OAAO,EAAE,aAAa,EAAE,CAAC"}
package/dist/index.js CHANGED
@@ -16,7 +16,7 @@
16
16
  import { PLUGIN_VERSION, DEFAULT_API_BASE, SIDECAR_PORT } from "./constants.js";
17
17
  import { log, LOG_FILE } from "./logger.js";
18
18
  import { MemoryXPlugin, getSDK } from "./plugin-core.js";
19
- import { extractProviderCredentials, getDefaultModelAndProvider, buildRealUpstreamBaseUrlMap, realUpstreamCredentialsForSidecar, loadRealUpstreamBaseUrlCache, saveRealUpstreamBaseUrlCache, isLocalhostBaseUrl, } from "./proxy-credentials.js";
19
+ import { extractProviderCredentials, getDefaultModelAndProvider, realUpstreamCredentialsForSidecar, syncRealUpstreamBaseUrlCacheFromConfig, } from "./proxy-credentials.js";
20
20
  import { createProxyRedirect } from "./proxy-redirect.js";
21
21
  import { SidecarServer } from "./sidecar.js";
22
22
  import { registerHooks } from "./hooks.js";
@@ -43,8 +43,7 @@ export default {
43
43
  log(`[Proxy] Default: ${defaultConfig.provider}/${defaultConfig.model}`);
44
44
  const realProviderHeader = "X-MemoryX-Real-Provider";
45
45
  const sidecarBaseUrl = `http://localhost:${SIDECAR_PORT}`;
46
- const realUpstreamBaseUrlMap = buildRealUpstreamBaseUrlMap(providerCredentials, sidecarBaseUrl);
47
- const realUpstreamCredentials = realUpstreamCredentialsForSidecar(providerCredentials, realUpstreamBaseUrlMap);
46
+ const realUpstreamCredentials = realUpstreamCredentialsForSidecar(providerCredentials, new Map());
48
47
  const proxyUrl = (pluginConfig?.apiBaseUrl || DEFAULT_API_BASE) + "/llm/proxy/chat/completions";
49
48
  const sidecar = new SidecarServer(realUpstreamCredentials, { model: defaultConfig.model, provider: defaultConfig.provider }, defaultConfig.availableProviders, {
50
49
  proxyUrl,
@@ -54,7 +53,10 @@ export default {
54
53
  });
55
54
  const getSidecarBase = () => `http://localhost:${sidecar.getPort()}`;
56
55
  const { applySidecarRedirect, wrapProvidersWithProxy } = createProxyRedirect(api, getSidecarBase, realProviderHeader);
57
- registerHooks(api, plugin, wrapProvidersWithProxy, applySidecarRedirect);
56
+ /** 仅当用户当前默认使用的是 memoryx-gateway 时才做拦截/注入,否则不碰配置,避免装完插件就用不了 OpenClaw。 */
57
+ const shouldApplyProxy = () => getDefaultModelAndProvider(extractProviderCredentials(api.config), api.config).provider === "memoryx-gateway";
58
+ const syncRealUpstreamBaseUrlCache = () => syncRealUpstreamBaseUrlCacheFromConfig(api.config, getSidecarBase());
59
+ registerHooks(api, plugin, wrapProvidersWithProxy, applySidecarRedirect, shouldApplyProxy, syncRealUpstreamBaseUrlCache);
58
60
  api.registerService({
59
61
  id: "memoryx-sidecar",
60
62
  start: async () => {
@@ -71,9 +73,14 @@ export default {
71
73
  api.logger.info("[MemoryX] Sidecar stopped");
72
74
  },
73
75
  });
74
- wrapProvidersWithProxy();
75
- applySidecarRedirect();
76
- api.logger.info(`[MemoryX] ✅ Plugin v${PLUGIN_VERSION} ready! Requests go through MemoryX proxy with your configured model.`);
76
+ if (defaultConfig.provider === "memoryx-gateway") {
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
+ }
77
84
  setImmediate(async () => {
78
85
  try {
79
86
  await plugin.init();
@@ -86,19 +93,7 @@ export default {
86
93
  // ignore
87
94
  }
88
95
  try {
89
- const cached = await loadRealUpstreamBaseUrlCache();
90
- const merged = new Map(realUpstreamBaseUrlMap);
91
- for (const [id, url] of cached) {
92
- const current = merged.get(id);
93
- if (current && isLocalhostBaseUrl(current, sidecarBaseUrl) && url) {
94
- merged.set(id, url);
95
- log(`[Proxy] Restored ${id} baseUrl from cache (config was localhost)`);
96
- }
97
- }
98
- if (cached.size > 0 && merged.size > 0) {
99
- sidecar.updateRealUpstreamBaseUrlMap(merged);
100
- }
101
- await saveRealUpstreamBaseUrlCache(merged);
96
+ await syncRealUpstreamBaseUrlCacheFromConfig(api.config, sidecarBaseUrl);
102
97
  }
103
98
  catch (e) {
104
99
  // ignore
@@ -1 +1 @@
1
- {"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"AAUA,eAAO,MAAM,QAAQ,QAAsC,CAAC;AAQ5D;;;GAGG;AACH,wBAAgB,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAA;CAAO,GAAG,IAAI,CA8B9E"}
1
+ {"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"AAaA,eAAO,MAAM,QAAQ,QAAsC,CAAC;AA4B5D;;;GAGG;AACH,wBAAgB,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAA;CAAO,GAAG,IAAI,CAiD9E"}
package/dist/logger.js CHANGED
@@ -1,17 +1,44 @@
1
1
  /**
2
2
  * Layered logging: file always, optional console for important messages.
3
+ * Log file size is capped: when plugin.log exceeds MAX_LOG_FILE_BYTES it is rotated to plugin.log.old and a new file is started.
3
4
  */
4
5
  import * as fs from "fs";
5
6
  import * as path from "path";
6
- import { PLUGIN_DIR } from "./constants.js";
7
+ import { PLUGIN_DIR, MAX_LOG_FILE_BYTES } from "./constants.js";
7
8
  let logStream = null;
8
9
  let logStreamReady = false;
10
+ let writeCount = 0;
11
+ const CHECK_ROTATE_EVERY = 500;
9
12
  export const LOG_FILE = path.join(PLUGIN_DIR, "plugin.log");
13
+ const LOG_FILE_OLD = path.join(PLUGIN_DIR, "plugin.log.old");
10
14
  function ensureDir() {
11
15
  if (!fs.existsSync(PLUGIN_DIR)) {
12
16
  fs.mkdirSync(PLUGIN_DIR, { recursive: true });
13
17
  }
14
18
  }
19
+ function rotateIfNeeded() {
20
+ try {
21
+ if (!fs.existsSync(LOG_FILE))
22
+ return;
23
+ const stat = fs.statSync(LOG_FILE);
24
+ if (stat.size < MAX_LOG_FILE_BYTES)
25
+ return;
26
+ if (logStream) {
27
+ try {
28
+ logStream.end();
29
+ }
30
+ catch (_) { }
31
+ logStream = null;
32
+ logStreamReady = false;
33
+ }
34
+ if (fs.existsSync(LOG_FILE_OLD))
35
+ fs.unlinkSync(LOG_FILE_OLD);
36
+ fs.renameSync(LOG_FILE, LOG_FILE_OLD);
37
+ }
38
+ catch (_) {
39
+ /* ignore */
40
+ }
41
+ }
15
42
  /**
16
43
  * Layered logging: all logs go to file (stream when ready, else sync append so nothing is lost).
17
44
  * When console=true, also log to console (for important/guidance messages).
@@ -26,8 +53,28 @@ export function log(message, options = {}) {
26
53
  try {
27
54
  if (!logStreamReady) {
28
55
  ensureDir();
56
+ rotateIfNeeded();
29
57
  logStream = fs.createWriteStream(LOG_FILE, { flags: "a" });
30
58
  logStreamReady = true;
59
+ writeCount = 0;
60
+ }
61
+ writeCount++;
62
+ if (writeCount >= CHECK_ROTATE_EVERY) {
63
+ writeCount = 0;
64
+ try {
65
+ if (fs.existsSync(LOG_FILE)) {
66
+ const stat = fs.statSync(LOG_FILE);
67
+ if (stat.size >= MAX_LOG_FILE_BYTES) {
68
+ rotateIfNeeded();
69
+ if (!logStreamReady) {
70
+ ensureDir();
71
+ logStream = fs.createWriteStream(LOG_FILE, { flags: "a" });
72
+ logStreamReady = true;
73
+ }
74
+ }
75
+ }
76
+ }
77
+ catch (_) { }
31
78
  }
32
79
  logStream?.write(line, (err) => {
33
80
  if (err) {
@@ -19,7 +19,7 @@ export declare function extractProviderCredentials(config: any, providerOverride
19
19
  * 在 init 时从 OpenClaw 配置(api.config)读到的「所有厂商 -> baseUrl」做一次快照,不写死任何厂商地址。
20
20
  * - 来源唯一:插件启动时通过 api 读到的 openclaw 配置文件(openclaw.json 等)里的 models.providers[id].baseUrl。
21
21
  * - 若某条已是 localhost(例如上次 redirect 被持久化),本次仍写入 localhost;恢复依赖 setImmediate 里从缓存文件合并(首次正常时已落盘)。
22
- * 返回 Map<providerId, baseUrl>,memoryx-proxy 不写入。
22
+ * 返回 Map<providerId, baseUrl>,memoryx-gateway 不写入。
23
23
  */
24
24
  export declare function buildRealUpstreamBaseUrlMap(credentials: Map<string, ProviderCredentials>, _sidecarBaseUrl: string): Map<string, string>;
25
25
  /**
@@ -30,8 +30,18 @@ export declare function buildRealUpstreamBaseUrlMap(credentials: Map<string, Pro
30
30
  export declare function realUpstreamCredentialsForSidecar(credentials: Map<string, ProviderCredentials>, realUpstreamBaseUrlMap: Map<string, string>): Map<string, ProviderCredentials>;
31
31
  /**
32
32
  * 从缓存文件加载「厂商 -> 真实 baseUrl」映射。文件不存在或解析失败返回空 Map。
33
+ * 供插件 sync 时使用(先读已有缓存,没有则当空)。
33
34
  */
34
35
  export declare function loadRealUpstreamBaseUrlCache(): Promise<Map<string, string>>;
36
+ /**
37
+ * 从缓存文件加载「厂商 -> 真实 baseUrl」映射;文件不存在、IO 或解析错误时 throw。
38
+ * 供 Sidecar 每次请求读盘使用,不缓存。
39
+ */
40
+ export declare function loadRealUpstreamBaseUrlCacheStrict(): Promise<Map<string, string>>;
41
+ /**
42
+ * 根据 OpenClaw 配置更新 real-upstream-baseurl.json:非 localhost 的 baseUrl 写入/覆盖,localhost 绝不覆盖。
43
+ */
44
+ export declare function syncRealUpstreamBaseUrlCacheFromConfig(config: any, sidecarBaseUrl: string): Promise<void>;
35
45
  /**
36
46
  * 将「厂商 -> 真实 baseUrl」映射写入缓存文件,重启后若配置被改成 localhost 可从此恢复。
37
47
  */
@@ -1 +1 @@
1
- {"version":3,"file":"proxy-credentials.d.ts","sourceRoot":"","sources":["../src/proxy-credentials.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAuCtD,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,CA0ClC;AAED;;;;;GAKG;AACH,wBAAgB,2BAA2B,CACvC,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,mBAAmB,CAAC,EAC7C,eAAe,EAAE,MAAM,GACxB,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAQrB;AAED;;;;GAIG;AACH,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;;GAEG;AACH,wBAAsB,4BAA4B,IAAI,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAcjF;AAED;;GAEG;AACH,wBAAsB,4BAA4B,CAAC,GAAG,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAgB1F;AAED,6DAA6D;AAC7D,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAI/E;AAOD,wGAAwG;AACxG,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,CAc5C;AAED;;;GAGG;AACH,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,CAU5C;AAoBD,+HAA+H;AAC/H,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
+ {"version":3,"file":"proxy-credentials.d.ts","sourceRoot":"","sources":["../src/proxy-credentials.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAuCtD,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,CAiDlC;AAED;;;;;GAKG;AACH,wBAAgB,2BAA2B,CACvC,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,mBAAmB,CAAC,EAC7C,eAAe,EAAE,MAAM,GACxB,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAQrB;AAED;;;;GAIG;AACH,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;;;GAGG;AACH,wBAAsB,4BAA4B,IAAI,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAcjF;AAED;;;GAGG;AACH,wBAAsB,kCAAkC,IAAI,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAUvF;AAED;;GAEG;AACH,wBAAsB,sCAAsC,CACxD,MAAM,EAAE,GAAG,EACX,cAAc,EAAE,MAAM,GACvB,OAAO,CAAC,IAAI,CAAC,CAiBf;AAED;;GAEG;AACH,wBAAsB,4BAA4B,CAAC,GAAG,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAgB1F;AAED,6DAA6D;AAC7D,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAI/E;AAOD,wGAAwG;AACxG,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,CAc5C;AAED;;;GAGG;AACH,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,CAU5C;AAoBD,+HAA+H;AAC/H,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"}
@@ -35,9 +35,6 @@ function loadAuthProfilesKeys(profilesPath) {
35
35
  export function extractProviderCredentials(config, providerOverrides) {
36
36
  const credentials = new Map();
37
37
  const authKeys = loadAuthProfilesKeys(DEFAULT_AUTH_PROFILES_PATH);
38
- if (authKeys.size > 0) {
39
- log(`[Proxy] Loaded apiKey from auth-profiles for providers: ${[...authKeys.keys()].join(", ")}`);
40
- }
41
38
  if (config?.models?.providers) {
42
39
  for (const [id, providerConfig] of Object.entries(config.models.providers)) {
43
40
  if (typeof providerConfig !== "object" ||
@@ -56,28 +53,35 @@ export function extractProviderCredentials(config, providerOverrides) {
56
53
  process.env[`${id.toUpperCase()}_API_KEY`] ||
57
54
  authKeys.get(id) ||
58
55
  "";
59
- const isMemoryxProxy = (id === "memoryx-proxy");
56
+ const isMemoryxGateway = (id === "memoryx-gateway");
60
57
  credentials.set(id, {
61
- baseUrl: isMemoryxProxy ? "" : (override?.baseUrl || pc.baseUrl),
58
+ baseUrl: isMemoryxGateway ? "" : (override?.baseUrl || pc.baseUrl),
62
59
  apiKey,
63
60
  models: pc.models,
64
61
  });
65
- log(`[Proxy] Extracted credentials for ${id}: models=${pc.models?.length || 0}`);
66
62
  }
67
63
  }
68
64
  }
65
+ if (credentials.size > 0) {
66
+ const parts = [...credentials.entries()].map(([id, c]) => `${id}(${c.models?.length ?? 0})`);
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);
72
+ }
69
73
  return credentials;
70
74
  }
71
75
  /**
72
76
  * 在 init 时从 OpenClaw 配置(api.config)读到的「所有厂商 -> baseUrl」做一次快照,不写死任何厂商地址。
73
77
  * - 来源唯一:插件启动时通过 api 读到的 openclaw 配置文件(openclaw.json 等)里的 models.providers[id].baseUrl。
74
78
  * - 若某条已是 localhost(例如上次 redirect 被持久化),本次仍写入 localhost;恢复依赖 setImmediate 里从缓存文件合并(首次正常时已落盘)。
75
- * 返回 Map<providerId, baseUrl>,memoryx-proxy 不写入。
79
+ * 返回 Map<providerId, baseUrl>,memoryx-gateway 不写入。
76
80
  */
77
81
  export function buildRealUpstreamBaseUrlMap(credentials, _sidecarBaseUrl) {
78
82
  const map = new Map();
79
83
  for (const [id, creds] of credentials) {
80
- if (id === "memoryx-proxy")
84
+ if (id === "memoryx-gateway")
81
85
  continue;
82
86
  const base = (creds.baseUrl ?? "").trim();
83
87
  map.set(id, base);
@@ -92,7 +96,7 @@ export function buildRealUpstreamBaseUrlMap(credentials, _sidecarBaseUrl) {
92
96
  export function realUpstreamCredentialsForSidecar(credentials, realUpstreamBaseUrlMap) {
93
97
  const out = new Map();
94
98
  for (const [id, creds] of credentials) {
95
- if (id === "memoryx-proxy") {
99
+ if (id === "memoryx-gateway") {
96
100
  out.set(id, creds);
97
101
  continue;
98
102
  }
@@ -103,6 +107,7 @@ export function realUpstreamCredentialsForSidecar(credentials, realUpstreamBaseU
103
107
  }
104
108
  /**
105
109
  * 从缓存文件加载「厂商 -> 真实 baseUrl」映射。文件不存在或解析失败返回空 Map。
110
+ * 供插件 sync 时使用(先读已有缓存,没有则当空)。
106
111
  */
107
112
  export async function loadRealUpstreamBaseUrlCache() {
108
113
  try {
@@ -121,6 +126,45 @@ export async function loadRealUpstreamBaseUrlCache() {
121
126
  return new Map();
122
127
  }
123
128
  }
129
+ /**
130
+ * 从缓存文件加载「厂商 -> 真实 baseUrl」映射;文件不存在、IO 或解析错误时 throw。
131
+ * 供 Sidecar 每次请求读盘使用,不缓存。
132
+ */
133
+ export async function loadRealUpstreamBaseUrlCacheStrict() {
134
+ const raw = await fs.promises.readFile(REAL_UPSTREAM_BASEURL_CACHE_FILE, "utf8");
135
+ const obj = JSON.parse(raw);
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());
141
+ }
142
+ }
143
+ return map;
144
+ }
145
+ /**
146
+ * 根据 OpenClaw 配置更新 real-upstream-baseurl.json:非 localhost 的 baseUrl 写入/覆盖,localhost 绝不覆盖。
147
+ */
148
+ export async function syncRealUpstreamBaseUrlCacheFromConfig(config, sidecarBaseUrl) {
149
+ let cached;
150
+ try {
151
+ cached = await loadRealUpstreamBaseUrlCache();
152
+ }
153
+ catch {
154
+ cached = new Map();
155
+ }
156
+ const credentials = extractProviderCredentials(config);
157
+ const fromConfig = buildRealUpstreamBaseUrlMap(credentials, sidecarBaseUrl);
158
+ const merged = new Map(cached);
159
+ for (const [id, url] of fromConfig) {
160
+ if (id === "memoryx-gateway")
161
+ continue;
162
+ if (url && !isLocalhostBaseUrl(url, sidecarBaseUrl)) {
163
+ merged.set(id, url);
164
+ }
165
+ }
166
+ await saveRealUpstreamBaseUrlCache(merged);
167
+ }
124
168
  /**
125
169
  * 将「厂商 -> 真实 baseUrl」映射写入缓存文件,重启后若配置被改成 localhost 可从此恢复。
126
170
  */
@@ -129,7 +173,7 @@ export async function saveRealUpstreamBaseUrlCache(map) {
129
173
  await fs.promises.mkdir(PLUGIN_DIR, { recursive: true });
130
174
  const obj = {};
131
175
  for (const [id, url] of map) {
132
- if (id !== "memoryx-proxy" && url)
176
+ if (id !== "memoryx-gateway" && url)
133
177
  obj[id] = url;
134
178
  }
135
179
  await fs.promises.writeFile(REAL_UPSTREAM_BASEURL_CACHE_FILE, JSON.stringify(obj, null, 2), "utf8");
@@ -15,7 +15,7 @@ export function createProxyRedirect(api, getSidecarBase, realProviderHeader) {
15
15
  const providers = api.config.models.providers;
16
16
  let n = 0;
17
17
  for (const providerId of creds.keys()) {
18
- if (providerId === "memoryx-proxy")
18
+ if (providerId === "memoryx-gateway")
19
19
  continue;
20
20
  const p = providers[providerId];
21
21
  if (!p || typeof p !== "object")
@@ -33,7 +33,7 @@ export function createProxyRedirect(api, getSidecarBase, realProviderHeader) {
33
33
  }
34
34
  if (n > 0) {
35
35
  for (const providerId of creds.keys()) {
36
- if (providerId === "memoryx-proxy")
36
+ if (providerId === "memoryx-gateway")
37
37
  continue;
38
38
  const p = providers[providerId];
39
39
  const readBack = (p && typeof p === "object" && p.baseUrl) ? p.baseUrl : "<missing>";
@@ -52,7 +52,7 @@ export function createProxyRedirect(api, getSidecarBase, realProviderHeader) {
52
52
  const handler = {
53
53
  get(target, prop) {
54
54
  const val = target[prop];
55
- if (val && typeof val === "object" && creds.has(prop) && prop !== "memoryx-proxy") {
55
+ if (val && typeof val === "object" && creds.has(prop) && prop !== "memoryx-gateway") {
56
56
  return new Proxy(val, {
57
57
  get(t, k) {
58
58
  if (k === "baseUrl")
package/dist/sidecar.d.ts CHANGED
@@ -1,3 +1,6 @@
1
+ /**
2
+ * Local HTTP Sidecar: 仅转发到服务端 proxy(记忆注入),无本地回落。
3
+ */
1
4
  import type { ProviderCredentials } from "./types.js";
2
5
  import type { PluginConfig } from "./types.js";
3
6
  export type GetSDK = (config?: PluginConfig) => Promise<any>;
@@ -23,18 +26,12 @@ export declare class SidecarServer {
23
26
  start(): Promise<void>;
24
27
  stop(): Promise<void>;
25
28
  getPort(): number;
26
- /**
27
- * 用持久化/合并后的真实上游 baseUrl 映射更新 credentials,避免重启后配置已是 localhost 时仍用旧缓存。
28
- */
29
- updateRealUpstreamBaseUrlMap(map: Map<string, string>): void;
30
29
  /** For logging: redact headers (Authorization, x-api-key, etc. show as ***) */
31
30
  private redactHeaders;
32
31
  /** Build forward request headers from apiKey in OpenClaw style; does not overwrite user config */
33
32
  private buildForwardHeaders;
34
- /** 1) Exclude memoryx-proxy, use real provider config; 2) On proxy error fall back to direct; on direct error return error to caller. */
33
+ /** 仅转发到服务端 proxy(记忆注入),无本地回落。 */
35
34
  private handleRequest;
36
- private directRequest;
37
- private writeResponse;
38
35
  private readBody;
39
36
  }
40
37
  //# sourceMappingURL=sidecar.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"sidecar.d.ts","sourceRoot":"","sources":["../src/sidecar.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AACtD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAI/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;IAMjB;;OAEG;IACH,4BAA4B,CAAC,GAAG,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI;IAU5D,+EAA+E;IAC/E,OAAO,CAAC,aAAa;IAUrB,kGAAkG;IAClG,OAAO,CAAC,mBAAmB;IAa3B,yIAAyI;YAC3H,aAAa;YAoLb,aAAa;YAwBb,aAAa;IAqB3B,OAAO,CAAC,QAAQ;CAQnB"}
1
+ {"version":3,"file":"sidecar.d.ts","sourceRoot":"","sources":["../src/sidecar.ts"],"names":[],"mappings":"AAAA;;GAEG;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;IAMjB,+EAA+E;IAC/E,OAAO,CAAC,aAAa;IAUrB,kGAAkG;IAClG,OAAO,CAAC,mBAAmB;IAa3B,iCAAiC;YACnB,aAAa;IA6O3B,OAAO,CAAC,QAAQ;CAQnB"}
package/dist/sidecar.js CHANGED
@@ -1,9 +1,10 @@
1
1
  /**
2
- * Local HTTP Sidecar: receives OpenClaw requests, forwards to MemoryX proxy or falls back to direct provider.
2
+ * Local HTTP Sidecar: 仅转发到服务端 proxy(记忆注入),无本地回落。
3
3
  */
4
4
  import * as http from "http";
5
5
  import { SIDECAR_PORT } from "./constants.js";
6
6
  import { log, LOG_FILE } from "./logger.js";
7
+ import { loadRealUpstreamBaseUrlCacheStrict } from "./proxy-credentials.js";
7
8
  export class SidecarServer {
8
9
  server = null;
9
10
  credentials;
@@ -58,18 +59,6 @@ export class SidecarServer {
58
59
  ? this.server.address().port
59
60
  : SIDECAR_PORT;
60
61
  }
61
- /**
62
- * 用持久化/合并后的真实上游 baseUrl 映射更新 credentials,避免重启后配置已是 localhost 时仍用旧缓存。
63
- */
64
- updateRealUpstreamBaseUrlMap(map) {
65
- for (const [id, baseUrl] of map) {
66
- const creds = this.credentials.get(id);
67
- if (creds && baseUrl) {
68
- this.credentials.set(id, { ...creds, baseUrl });
69
- }
70
- }
71
- log(`[Sidecar] Updated real upstream baseUrl map for ${map.size} provider(s)`);
72
- }
73
62
  /** For logging: redact headers (Authorization, x-api-key, etc. show as ***) */
74
63
  redactHeaders(h) {
75
64
  const out = {};
@@ -95,7 +84,7 @@ export class SidecarServer {
95
84
  }
96
85
  return h;
97
86
  }
98
- /** 1) Exclude memoryx-proxy, use real provider config; 2) On proxy error fall back to direct; on direct error return error to caller. */
87
+ /** 仅转发到服务端 proxy(记忆注入),无本地回落。 */
99
88
  async handleRequest(req, res) {
100
89
  const url = req.url || "/";
101
90
  const method = req.method?.toUpperCase();
@@ -106,12 +95,12 @@ export class SidecarServer {
106
95
  return;
107
96
  }
108
97
  const reqId = `req-${Date.now()}`;
109
- log(`[Sidecar] ${reqId} Incoming ${method} ${url}`);
110
98
  let body;
111
99
  try {
112
100
  body = await this.readBody(req);
113
101
  }
114
102
  catch (e) {
103
+ log(`[Sidecar] ${reqId} err read body: ${e?.message ?? "Read body failed"}`, { console: true });
115
104
  res.writeHead(502, { "Content-Type": "application/json" });
116
105
  res.end(JSON.stringify({ error: e?.message || "Read body failed" }));
117
106
  return;
@@ -123,11 +112,13 @@ export class SidecarServer {
123
112
  catch {
124
113
  openaiRequest = {};
125
114
  }
126
- const rawProvider = req.headers["x-memoryx-real-provider"]?.trim() || "";
115
+ const messages = openaiRequest.messages || [];
127
116
  const modelStr = openaiRequest.model || "";
117
+ const stream = !!openaiRequest.stream;
118
+ const rawProvider = req.headers["x-memoryx-real-provider"]?.trim() || "";
128
119
  let provider = rawProvider || (modelStr.includes("/") ? modelStr.split("/")[0] : "") || "";
129
- if (provider === "memoryx-proxy") {
130
- const real = this.availableProviders.find((p) => p.provider !== "memoryx-proxy");
120
+ if (provider === "memoryx-gateway") {
121
+ const real = this.availableProviders.find((p) => p.provider !== "memoryx-gateway");
131
122
  provider = real ? real.provider : "";
132
123
  }
133
124
  if (!provider || !this.credentials.has(provider)) {
@@ -135,34 +126,73 @@ export class SidecarServer {
135
126
  }
136
127
  if (!provider || !this.credentials.has(provider)) {
137
128
  for (const [id] of this.credentials) {
138
- if (id !== "memoryx-proxy") {
129
+ if (id !== "memoryx-gateway") {
139
130
  provider = id;
140
131
  break;
141
132
  }
142
133
  }
143
134
  }
144
135
  let creds = provider ? this.credentials.get(provider) : undefined;
145
- if (!creds?.baseUrl?.trim() || !(creds.apiKey ?? "").trim()) {
136
+ if (!creds || !(creds.apiKey ?? "").trim()) {
146
137
  for (const [id, c] of this.credentials) {
147
- if (id !== "memoryx-proxy" && (c.baseUrl || "").trim() && (c.apiKey ?? "").trim()) {
138
+ if (id !== "memoryx-gateway" && (c.apiKey ?? "").trim()) {
148
139
  provider = id;
149
140
  creds = c;
150
141
  break;
151
142
  }
152
143
  }
153
144
  }
154
- const baseUrl = (creds?.baseUrl || "").trim();
155
- const apiKey = (creds?.apiKey ?? "").trim();
156
- if (!baseUrl || !apiKey) {
145
+ let apiKey = (creds?.apiKey ?? "").trim();
146
+ if (!provider || !creds || !apiKey) {
157
147
  res.writeHead(502, { "Content-Type": "application/json" });
158
- res.end(JSON.stringify({ error: "No upstream" }));
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
+ }
172
+ let baseUrlMap;
173
+ try {
174
+ baseUrlMap = await loadRealUpstreamBaseUrlCacheStrict();
175
+ }
176
+ catch (e) {
177
+ res.writeHead(502, { "Content-Type": "application/json" });
178
+ res.end(JSON.stringify({
179
+ error: "real-upstream-baseurl.json could not be read: " + (e?.message ?? String(e)),
180
+ }));
181
+ return;
182
+ }
183
+ const baseUrl = baseUrlMap.get(provider)?.trim();
184
+ if (!baseUrl) {
185
+ res.writeHead(502, { "Content-Type": "application/json" });
186
+ res.end(JSON.stringify({
187
+ error: `Missing real upstream baseUrl for provider '${provider}'. Ensure real-upstream-baseurl.json is populated (e.g. from OpenClaw config).`,
188
+ }));
159
189
  return;
160
190
  }
161
191
  const base = baseUrl.replace(/\/$/, "");
162
192
  const pathFromOpenClaw = (url.split("?")[0] || "/").trim() || "/";
163
193
  const targetUrl = base + (pathFromOpenClaw.startsWith("/") ? pathFromOpenClaw : "/" + pathFromOpenClaw);
164
194
  const currentModel = openaiRequest.model || "";
165
- if (!currentModel || currentModel === "auto" || currentModel.startsWith("memoryx-proxy/")) {
195
+ if (!currentModel || currentModel === "auto" || currentModel.startsWith("memoryx-gateway/")) {
166
196
  const firstId = creds?.models?.[0]?.id || creds?.models?.[0]?.name;
167
197
  if (firstId)
168
198
  openaiRequest.model = firstId;
@@ -176,25 +206,21 @@ export class SidecarServer {
176
206
  agentId = accountInfo.agentId || "";
177
207
  }
178
208
  catch (_e) {
179
- /* Do not block; proxy will fail and then fall back to direct */
209
+ /* Do not block; proxy may still accept request */
180
210
  }
181
211
  const headers = this.buildForwardHeaders(provider, apiKey);
182
- const messages = openaiRequest.messages || [];
183
212
  const lastUserMsg = [...messages].reverse().find((m) => m.role === "user");
184
213
  const searchQuery = typeof lastUserMsg?.content === "string" ? lastUserMsg.content : "";
185
214
  const proxyRequestBody = {
186
215
  targetUrl,
216
+ api_base: base,
187
217
  headers,
188
218
  body: openaiRequest,
189
219
  searchQuery,
190
220
  agent_id: agentId,
191
221
  available_models: this.availableProviders.map((p) => `${p.provider}/${p.model}`),
192
222
  };
193
- const proxyReqHeaders = { "Content-Type": "application/json", "X-API-Key": memoryxApiKey ? "***" : "" };
194
- log(`[Sidecar] ${reqId} → Proxy POST ${proxyUrl}`);
195
- log(`[Sidecar] ${reqId} Proxy request headers: ${JSON.stringify(proxyReqHeaders)}`);
196
- log(`[Sidecar] ${reqId} Proxy body: targetUrl=${targetUrl} model=${openaiRequest.model} stream=${!!openaiRequest.stream} searchQuery.length=${searchQuery.length} agent_id=${agentId || "(empty)"}`);
197
- log(`[Sidecar] ${reqId} Forward headers (to upstream): ${JSON.stringify(this.redactHeaders(headers))}`);
223
+ log(`[Sidecar] ${reqId} ${method} ${(url.split("?")[0] || "/").trim()} provider=${provider} model=${openaiRequest.model ?? "-"} stream=${stream} proxy target=${targetUrl}`);
198
224
  let proxyResponse = null;
199
225
  try {
200
226
  proxyResponse = await fetch(proxyUrl, {
@@ -205,16 +231,28 @@ export class SidecarServer {
205
231
  }
206
232
  catch (e) {
207
233
  proxyResponse = null;
208
- log(`[Sidecar] ${reqId} Proxy fetch error: ${e?.message ?? String(e)}`, { console: true });
209
- }
210
- if (proxyResponse) {
211
- log(`[Sidecar] ${reqId} Proxy response status=${proxyResponse.status} ${proxyResponse.statusText}`);
234
+ log(`[Sidecar] ${reqId} err proxy fetch: ${e?.message ?? String(e)}`, { console: true });
212
235
  }
213
236
  const proxyFailed = !proxyResponse || proxyResponse.status >= 500;
214
237
  if (proxyFailed) {
215
- log(`[Sidecar] ${reqId} Proxy failed, fallback to direct POST ${targetUrl}`, { console: true });
216
- const directResponse = await this.directRequest(reqId, targetUrl, headers, openaiRequest);
217
- this.writeResponse(res, directResponse);
238
+ if (proxyResponse?.body) {
239
+ try {
240
+ if (typeof proxyResponse.body.cancel === "function") {
241
+ await proxyResponse.body.cancel();
242
+ }
243
+ else {
244
+ await proxyResponse.text();
245
+ }
246
+ }
247
+ catch (_) {
248
+ /* drain failed, ignore */
249
+ }
250
+ }
251
+ const status = proxyResponse?.status ?? 502;
252
+ const detail = proxyResponse?.statusText || "MemoryX proxy unavailable";
253
+ log(`[Sidecar] ${reqId} ← ${status} proxy failed: ${detail}`, { console: true });
254
+ res.writeHead(status, { "Content-Type": "application/json" });
255
+ res.end(JSON.stringify({ error: detail }));
218
256
  return;
219
257
  }
220
258
  const proxy = proxyResponse;
@@ -229,11 +267,9 @@ export class SidecarServer {
229
267
  }
230
268
  catch (firstErr) {
231
269
  reader.releaseLock();
232
- log(`[Sidecar] ${reqId} Proxy stream first chunk failed: ${firstErr?.message ?? "timeout"}, fallback to direct ${targetUrl}`, { console: true });
233
- const directResponse = await this.directRequest(reqId, targetUrl, headers, openaiRequest);
234
- if (directResponse)
235
- log(`[Sidecar] ${reqId} Direct response status=${directResponse.status}`);
236
- this.writeResponse(res, directResponse);
270
+ log(`[Sidecar] ${reqId} err stream first chunk: ${firstErr?.message ?? "timeout"}`, { console: true });
271
+ res.writeHead(502, { "Content-Type": "application/json" });
272
+ res.end(JSON.stringify({ error: "MemoryX proxy stream failed" }));
237
273
  return;
238
274
  }
239
275
  res.writeHead(proxy.status, {
@@ -253,7 +289,7 @@ export class SidecarServer {
253
289
  reader.releaseLock();
254
290
  }
255
291
  res.end();
256
- log(`[Sidecar] ${reqId} Proxy stream done, status=${proxy.status}`);
292
+ log(`[Sidecar] ${reqId} ${proxy.status} stream`);
257
293
  return;
258
294
  }
259
295
  if (!proxy.body) {
@@ -261,56 +297,16 @@ export class SidecarServer {
261
297
  "Content-Type": proxy.headers.get("content-type") || "application/json",
262
298
  });
263
299
  res.end();
300
+ log(`[Sidecar] ${reqId} ← ${proxy.status} (no body)`);
264
301
  return;
265
302
  }
266
303
  const text = await proxy.text();
267
- log(`[Sidecar] ${reqId} Proxy done, status=${proxy.status} body.length=${text.length}`);
304
+ log(`[Sidecar] ${reqId} ${proxy.status} body=${text.length}`);
268
305
  res.writeHead(proxy.status, {
269
306
  "Content-Type": proxy.headers.get("content-type") || "application/json",
270
307
  });
271
308
  res.end(text);
272
309
  }
273
- async directRequest(reqId, targetUrl, headers, openaiRequest) {
274
- log(`[Sidecar] ${reqId} Direct POST ${targetUrl} headers=${JSON.stringify(this.redactHeaders(headers))} model=${openaiRequest?.model} stream=${!!openaiRequest?.stream}`);
275
- try {
276
- const resp = await fetch(targetUrl, {
277
- method: "POST",
278
- headers,
279
- body: JSON.stringify(openaiRequest),
280
- });
281
- log(`[Sidecar] ${reqId} Direct response status=${resp.status} ${resp.statusText}`);
282
- return resp;
283
- }
284
- catch (e) {
285
- log(`[Sidecar] ${reqId} Direct fetch error: ${e?.message ?? String(e)}`, { console: true });
286
- return new Response(JSON.stringify({ error: e?.message || "Direct request failed" }), {
287
- status: 502,
288
- headers: { "Content-Type": "application/json" },
289
- });
290
- }
291
- }
292
- async writeResponse(res, directResponse) {
293
- const contentType = directResponse.headers.get("content-type") || "application/json";
294
- res.writeHead(directResponse.status, { "Content-Type": contentType });
295
- const body = directResponse.body;
296
- if (!body) {
297
- res.end();
298
- return;
299
- }
300
- const reader = body.getReader();
301
- try {
302
- while (true) {
303
- const { done, value } = await reader.read();
304
- if (done)
305
- break;
306
- res.write(Buffer.from(value));
307
- }
308
- }
309
- finally {
310
- reader.releaseLock();
311
- }
312
- res.end();
313
- }
314
310
  readBody(req) {
315
311
  return new Promise((resolve, reject) => {
316
312
  const chunks = [];
@@ -0,0 +1,14 @@
1
+ /**
2
+ * 与服务端 llm_proxy 一致的厂商请求体规范化。
3
+ * 部分 LLM 厂商非标准 OpenAI 格式,需在此做兼容后再转发;服务端用 LiteLLM,Sidecar 直连时用本模块保证同一套逻辑。
4
+ */
5
+ /**
6
+ * 判断是否为 Z.AI GLM-5:按 provider 或 targetUrl 与 model 判断。
7
+ */
8
+ export declare function isZaiGlm5(provider: string, targetUrl: string, model: string): boolean;
9
+ /**
10
+ * 按厂商与 URL/model 规范化请求体;与服务端逻辑一致,供 Sidecar 直连时使用。
11
+ * 返回规范化后的 body(不修改入参)。
12
+ */
13
+ export declare function normalizeBodyForVendor(provider: string, targetUrl: string, body: Record<string, unknown>): Record<string, unknown>;
14
+ //# sourceMappingURL=vendor-normalize.d.ts.map
@@ -0,0 +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,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAQrF;AA4DD;;;GAGG;AACH,wBAAgB,sBAAsB,CAClC,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC9B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAMzB"}
@@ -0,0 +1,82 @@
1
+ /**
2
+ * 与服务端 llm_proxy 一致的厂商请求体规范化。
3
+ * 部分 LLM 厂商非标准 OpenAI 格式,需在此做兼容后再转发;服务端用 LiteLLM,Sidecar 直连时用本模块保证同一套逻辑。
4
+ */
5
+ /**
6
+ * 判断是否为 Z.AI GLM-5:按 provider 或 targetUrl 与 model 判断。
7
+ */
8
+ export function isZaiGlm5(provider, targetUrl, model) {
9
+ const prov = (provider || "").toLowerCase();
10
+ const url = (targetUrl || "").toLowerCase();
11
+ const m = (model || "").toLowerCase();
12
+ if (prov === "zai" && (m === "glm-5" || m.endsWith("/glm-5")))
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;
17
+ }
18
+ /**
19
+ * Z.AI GLM-5 兼容:与 api/app/routers/llm_proxy.py _normalize_body_for_zai_glm5 保持一致。
20
+ * - messages 仅保留 role + content,role 仅允许 system/user/assistant
21
+ * - 顶层 system 并入 messages 首条 role=system
22
+ * - OpenClaw 的 developer 视为 system,其他非法 role 归为 user
23
+ * - 可选 thinking:未传时默认 {"type": "enabled"}
24
+ */
25
+ function normalizeBodyForZaiGlm5(body) {
26
+ const out = { ...body };
27
+ delete out.system;
28
+ const rawMessages = Array.isArray(out.messages) ? [...out.messages] : [];
29
+ let systemContent = (typeof body.system === "string" ? body.system : "").trim();
30
+ if (systemContent) {
31
+ const existingSystem = rawMessages.find((m) => (String(m?.role || "").toLowerCase() === "system"));
32
+ if (existingSystem && typeof existingSystem.content === "string") {
33
+ systemContent = `${existingSystem.content}\n\n${systemContent}`.trim();
34
+ }
35
+ }
36
+ const withoutSystem = rawMessages.filter((m) => String(m?.role || "").toLowerCase() !== "system");
37
+ const ROLE_MAP = { developer: "system" };
38
+ const messages = [];
39
+ for (const m of withoutSystem) {
40
+ let r = String(m.role || "").trim().toLowerCase();
41
+ if (r === "system" || r === "user" || r === "assistant") {
42
+ // keep
43
+ }
44
+ else if (ROLE_MAP[r]) {
45
+ r = ROLE_MAP[r];
46
+ }
47
+ else {
48
+ r = "user";
49
+ }
50
+ let content = m.content;
51
+ if (content == null)
52
+ content = "";
53
+ if (Array.isArray(content)) {
54
+ content = content
55
+ .map((part) => typeof part === "object" && part != null && "text" in part
56
+ ? String(part.text)
57
+ : String(part))
58
+ .join("\n");
59
+ }
60
+ const contentStr = String(content).trim();
61
+ messages.push({ role: r, content: contentStr || " " });
62
+ }
63
+ if (systemContent) {
64
+ messages.unshift({ role: "system", content: systemContent });
65
+ }
66
+ out.messages = messages;
67
+ if (!("thinking" in out) || out.thinking == null) {
68
+ out.thinking = { type: "enabled" };
69
+ }
70
+ return out;
71
+ }
72
+ /**
73
+ * 按厂商与 URL/model 规范化请求体;与服务端逻辑一致,供 Sidecar 直连时使用。
74
+ * 返回规范化后的 body(不修改入参)。
75
+ */
76
+ export function normalizeBodyForVendor(provider, targetUrl, body) {
77
+ const model = String(body?.model || "").trim();
78
+ if (isZaiGlm5(provider, targetUrl, model)) {
79
+ return normalizeBodyForZaiGlm5(body);
80
+ }
81
+ return body;
82
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@t0ken.ai/memoryx-openclaw-plugin",
3
- "version": "2.2.61",
3
+ "version": "2.2.63",
4
4
  "description": "MemoryX real-time memory capture and recall plugin for OpenClaw (powered by @t0ken.ai/memoryx-sdk)",
5
5
  "type": "module",
6
6
  "author": "MemoryX Team",
@@ -15,7 +15,7 @@
15
15
  "prebuild": "node scripts/update-version.cjs",
16
16
  "build": "tsc",
17
17
  "test:recall": "node scripts/test-recall-capture.mjs",
18
- "reset-proxy-usage": "node scripts/reset-memoryx-proxy-usage.mjs"
18
+ "reset-gateway-usage": "node scripts/reset-memoryx-gateway-usage.mjs"
19
19
  },
20
20
  "keywords": [
21
21
  "openclaw",