@t0ken.ai/memoryx-openclaw-plugin 2.2.68 → 2.2.70

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.
@@ -1,4 +1,4 @@
1
- export declare const PLUGIN_VERSION = "2.2.68";
1
+ export declare const PLUGIN_VERSION = "2.2.70";
2
2
  export declare const DEFAULT_API_BASE = "https://t0ken.ai/api";
3
3
  export declare const PLUGIN_DIR: string;
4
4
  /** 真实上游 baseUrl 缓存文件:重启后若配置已被改成 localhost,从此文件恢复各厂商真实地址。 */
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.68";
7
+ export const PLUGIN_VERSION = "2.2.70";
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
@@ -1,5 +1,6 @@
1
1
  /**
2
- * OpenClaw hooks: message_received, llm_input, llm_output, before_agent_start, session_end.
2
+ * OpenClaw hooks: message_received, llm_input, llm_output,
3
+ * before_model_resolve, before_agent_start, session_end.
3
4
  */
4
5
  import type { MemoryXPlugin } from "./plugin-core.js";
5
6
  export declare function registerHooks(api: any, plugin: MemoryXPlugin, wrapProvidersWithProxy: () => void, applySidecarRedirect: (opts?: {
@@ -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,GAChC,IAAI,CAoCN"}
1
+ {"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../src/hooks.ts"],"names":[],"mappings":"AAAA;;;GAGG;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,13 +1,10 @@
1
+ import { log } from "./logger.js";
1
2
  export function registerHooks(api, plugin, wrapProvidersWithProxy, applySidecarRedirect, shouldApplyProxy) {
2
- let useVirtualProviderInLastRun = false;
3
3
  api.on("message_received", async (event) => {
4
4
  const { content } = event;
5
5
  if (content && plugin)
6
6
  await plugin.onMessage("user", content);
7
7
  });
8
- api.on("llm_input", (_event) => {
9
- useVirtualProviderInLastRun = _event?.provider === "memoryx-gateway";
10
- });
11
8
  api.on("llm_output", async (event) => {
12
9
  const { assistantTexts } = event;
13
10
  if (assistantTexts && Array.isArray(assistantTexts) && plugin) {
@@ -16,11 +13,18 @@ export function registerHooks(api, plugin, wrapProvidersWithProxy, applySidecarR
16
13
  await plugin.onMessage("assistant", fullContent);
17
14
  }
18
15
  });
16
+ // Override model to "auto" (which doesn't exist in models.json) so
17
+ // resolveModel() falls through to the config fallback path. The fallback
18
+ // reads baseUrl from cfg.models.providers[provider] — which we patch to
19
+ // the sidecar URL in runtimeConfigSnapshot. This preserves the original
20
+ // provider (correct API key + protocol) while routing through the sidecar.
21
+ api.on("before_model_resolve", async () => {
22
+ if (!shouldApplyProxy())
23
+ return undefined;
24
+ log(`[Proxy] before_model_resolve → modelOverride=auto (provider preserved)`);
25
+ return { modelOverride: "auto" };
26
+ });
19
27
  api.on("before_agent_start", async (_event) => {
20
- if (shouldApplyProxy()) {
21
- wrapProvidersWithProxy();
22
- applySidecarRedirect({ quiet: true });
23
- }
24
28
  try {
25
29
  await plugin.startTimersIfNeeded();
26
30
  }
@@ -1 +1 @@
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"}
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;;;;;;kBAwBvC,GAAG,iBAAiB,YAAY,GAAG,IAAI;;AARzD,wBAwIE;AAEF,OAAO,EAAE,aAAa,EAAE,CAAC"}
package/dist/index.js CHANGED
@@ -18,7 +18,7 @@
18
18
  import { PLUGIN_VERSION, DEFAULT_API_BASE, SIDECAR_PORT } from "./constants.js";
19
19
  import { log, LOG_FILE } from "./logger.js";
20
20
  import { MemoryXPlugin, getSDK } from "./plugin-core.js";
21
- import { extractProviderCredentials, getDefaultModelAndProvider, realUpstreamCredentialsForSidecar, syncRealUpstreamBaseUrlCacheFromConfig, } from "./proxy-credentials.js";
21
+ import { extractProviderCredentials, getDefaultModelAndProvider, buildRealUpstreamBaseUrlMap, realUpstreamCredentialsForSidecar, saveRealUpstreamBaseUrlCache, loadRealUpstreamBaseUrlCache, } from "./proxy-credentials.js";
22
22
  import { createProxyRedirect } from "./proxy-redirect.js";
23
23
  import { SidecarServer } from "./sidecar.js";
24
24
  import { registerHooks } from "./hooks.js";
@@ -39,7 +39,6 @@ export default {
39
39
  }
40
40
  plugin = new MemoryXPlugin(pluginConfig);
41
41
  registerTools(api, plugin);
42
- // Pure memory: read provider list from api.config (no file I/O, same as slimclaw)
43
42
  const configCredentials = extractProviderCredentials(api.config);
44
43
  log(`[Proxy] Found ${configCredentials.size} providers in config`);
45
44
  const defaultConfig = getDefaultModelAndProvider(configCredentials, api.config);
@@ -50,21 +49,58 @@ export default {
50
49
  let sidecar = null;
51
50
  const getSidecarBase = () => sidecar ? `http://localhost:${sidecar.getPort()}` : sidecarBaseUrl;
52
51
  const { applySidecarRedirect, wrapProvidersWithProxy } = createProxyRedirect(api, getSidecarBase, realProviderHeader);
53
- const shouldApplyProxy = () => {
54
- const creds = extractProviderCredentials(api.config);
55
- return getDefaultModelAndProvider(creds, api.config).provider === "memoryx-gateway";
56
- };
52
+ // Capture real upstream URLs (sync, no I/O).
53
+ // Do NOT call applySidecarRedirect() here — it mutates runtimeConfigSnapshot
54
+ // and makes writeConfigFile's merge-patch compute an empty diff.
55
+ const savedRealUpstreamUrls = buildRealUpstreamBaseUrlMap(configCredentials, sidecarBaseUrl);
56
+ const shouldApplyProxy = () => true;
57
57
  registerHooks(api, plugin, wrapProvidersWithProxy, applySidecarRedirect, shouldApplyProxy);
58
58
  api.registerService({
59
59
  id: "memoryx-sidecar",
60
60
  start: async () => {
61
61
  try {
62
- // Re-read credentials from api.config (pure memory, may have updated since register)
62
+ // 1. Save real upstream URLs to disk cache (for sidecar forwarding)
63
+ let cached;
64
+ try {
65
+ cached = await loadRealUpstreamBaseUrlCache();
66
+ }
67
+ catch {
68
+ cached = new Map();
69
+ }
70
+ for (const [id, url] of savedRealUpstreamUrls) {
71
+ if (id !== "memoryx-gateway" && url && !url.includes("localhost")) {
72
+ cached.set(id, url);
73
+ }
74
+ }
75
+ await saveRealUpstreamBaseUrlCache(cached);
76
+ // 2. Patch runtimeConfigSnapshot so resolveModel()'s config
77
+ // fallback path uses sidecar baseUrl. api.config and
78
+ // runtimeConfigSnapshot are separate clones — we must
79
+ // patch the one loadConfig() returns.
80
+ const runtimeSnapshot = api.runtime?.config?.loadConfig?.();
81
+ if (runtimeSnapshot?.models?.providers) {
82
+ const providers = runtimeSnapshot.models.providers;
83
+ let n = 0;
84
+ for (const [id, p] of Object.entries(providers)) {
85
+ if (id === "memoryx-gateway" || !p || typeof p !== "object")
86
+ continue;
87
+ const cur = (p.baseUrl || "").trim();
88
+ if (cur && !cur.includes("localhost")) {
89
+ p.baseUrl = `${sidecarBaseUrl}/_p/${id}`;
90
+ n++;
91
+ }
92
+ }
93
+ if (n > 0)
94
+ log(`[Proxy] Patched runtimeConfigSnapshot: ${n} provider(s) → Sidecar`);
95
+ }
96
+ // 3. Start sidecar with real upstream credentials.
97
+ // API keys are in OpenClaw's AuthStorage (auth.json),
98
+ // NOT in config — credentials will have empty apiKey.
99
+ // The sidecar uses the incoming request's Authorization
100
+ // header instead, passed as incoming_api_key to the gateway.
63
101
  const credentials = extractProviderCredentials(api.config);
64
102
  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());
103
+ const realUpstreamCredentials = realUpstreamCredentialsForSidecar(credentials, savedRealUpstreamUrls);
68
104
  sidecar = new SidecarServer(realUpstreamCredentials, { model: defaults.model, provider: defaults.provider }, defaults.availableProviders, {
69
105
  proxyUrl,
70
106
  getSDK,
@@ -73,16 +109,25 @@ export default {
73
109
  });
74
110
  await sidecar.start();
75
111
  api.logger.info(`[MemoryX] Sidecar started on port ${sidecar.getPort()}`);
76
- if (defaults.provider === "memoryx-gateway") {
77
- wrapProvidersWithProxy();
78
- applySidecarRedirect();
79
- }
80
112
  }
81
113
  catch (e) {
82
114
  api.logger.error(`[MemoryX] Sidecar failed to start: ${e.message}`);
83
115
  }
84
116
  },
85
117
  stop: async () => {
118
+ // Restore runtimeConfigSnapshot baseUrls
119
+ try {
120
+ const realUrls = await loadRealUpstreamBaseUrlCache();
121
+ const snapshot = api.runtime?.config?.loadConfig?.();
122
+ if (snapshot?.models?.providers && realUrls.size > 0) {
123
+ for (const [id, url] of realUrls) {
124
+ const p = snapshot.models.providers[id];
125
+ if (p && typeof p === "object" && url)
126
+ p.baseUrl = url;
127
+ }
128
+ }
129
+ }
130
+ catch { /* best-effort */ }
86
131
  if (sidecar)
87
132
  await sidecar.stop();
88
133
  api.logger.info("[MemoryX] Sidecar stopped");
@@ -1 +1 @@
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"}
1
+ {"version":3,"file":"proxy-redirect.d.ts","sourceRoot":"","sources":["../src/proxy-redirect.ts"],"names":[],"mappings":"AAmBA,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,CAmEA"}
@@ -2,31 +2,26 @@
2
2
  * Redirect provider baseUrl to Sidecar and wrap config so reads always resolve to Sidecar.
3
3
  *
4
4
  * Neither function performs file I/O. They only mutate api.config in memory.
5
+ *
6
+ * IMPORTANT (OpenClaw config lifecycle):
7
+ * api.config is a direct reference to the config object at startup time.
8
+ * At startup, api.config === runtimeConfigSnapshot (same object), so mutations
9
+ * here are visible to loadConfig() and LLM requests.
10
+ * After a config hot-reload, loadConfig() returns a NEW cloned object, so the
11
+ * Proxy on api.config no longer affects LLM requests. This is an accepted
12
+ * limitation — only "plugins" config changes trigger a full gateway restart
13
+ * (re-running register()), while "models" changes are kind:"none" and rarely
14
+ * happen mid-session.
5
15
  */
6
16
  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
- }
17
+ const WRAPPED_MARKER = Symbol.for("memoryx-proxy-wrapped");
26
18
  export function createProxyRedirect(api, getSidecarBase, realProviderHeader) {
19
+ let redirectApplied = false;
27
20
  const applySidecarRedirect = (opts) => {
21
+ if (redirectApplied && opts?.quiet)
22
+ return;
28
23
  const sidecarBase = getSidecarBase();
29
- const providerIds = getProviderIds(api.config);
24
+ const providerIds = getProviderIdsFromRaw(api.config);
30
25
  if (providerIds.length === 0) {
31
26
  if (!opts?.quiet)
32
27
  log(`[Proxy] No providers to redirect`, { console: true });
@@ -43,23 +38,32 @@ export function createProxyRedirect(api, getSidecarBase, realProviderHeader) {
43
38
  p.headers = {};
44
39
  p.headers[realProviderHeader] = providerId;
45
40
  n++;
46
- log(`[Proxy] Redirected provider "${providerId}" baseUrl → Sidecar, header ${realProviderHeader}=${providerId}`);
47
41
  }
48
- if (n > 0 && !opts?.quiet) {
49
- api.logger.info(`[MemoryX] ✅ Sidecar redirect applied for ${n} provider(s).`);
42
+ if (n > 0) {
43
+ if (!opts?.quiet) {
44
+ api.logger.info(`[MemoryX] ✅ Sidecar redirect applied for ${n} provider(s).`);
45
+ }
46
+ if (!redirectApplied) {
47
+ log(`[Proxy] Redirected ${n} provider(s) baseUrl → Sidecar`);
48
+ }
49
+ redirectApplied = true;
50
50
  }
51
51
  };
52
52
  const wrapProvidersWithProxy = () => {
53
53
  if (!api.config?.models?.providers || typeof api.config.models.providers !== "object")
54
54
  return;
55
- const providerIds = new Set(getProviderIds(api.config));
55
+ const current = api.config.models.providers;
56
+ if (current[WRAPPED_MARKER])
57
+ return;
58
+ const providerIds = new Set(getProviderIdsFromRaw(api.config));
56
59
  if (providerIds.size === 0)
57
60
  return;
58
- const raw = api.config.models.providers;
59
61
  const handler = {
60
62
  get(target, prop) {
63
+ if (prop === WRAPPED_MARKER)
64
+ return true;
61
65
  const val = target[prop];
62
- if (val && typeof val === "object" && providerIds.has(prop)) {
66
+ if (val && typeof val === "object" && typeof prop === "string" && providerIds.has(prop)) {
63
67
  return new Proxy(val, {
64
68
  get(t, k) {
65
69
  if (k === "baseUrl")
@@ -76,8 +80,23 @@ export function createProxyRedirect(api, getSidecarBase, realProviderHeader) {
76
80
  return val;
77
81
  },
78
82
  };
79
- api.config.models.providers = new Proxy(raw, handler);
83
+ api.config.models.providers = new Proxy(current, handler);
80
84
  log(`[Proxy] Wrapped models.providers in Proxy so baseUrl/headers always resolve to Sidecar`);
81
85
  };
82
86
  return { applySidecarRedirect, wrapProvidersWithProxy };
83
87
  }
88
+ function getProviderIdsFromRaw(config) {
89
+ const providers = config?.models?.providers;
90
+ if (!providers || typeof providers !== "object")
91
+ return [];
92
+ const ids = [];
93
+ for (const id of Object.keys(providers)) {
94
+ if (id === "memoryx-gateway")
95
+ continue;
96
+ const p = providers[id];
97
+ if (p && typeof p === "object" && "baseUrl" in p) {
98
+ ids.push(id);
99
+ }
100
+ }
101
+ return ids;
102
+ }
package/dist/sidecar.d.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * Local HTTP Sidecar: lightweight forwarder.
3
- * Reads base_url_map from local cache file, credentials from constructor (api.config).
3
+ * Reads base_url_map from local cache file, credentials from constructor.
4
4
  * Packages payload and POSTs to MemoryX API server.
5
+ * On gateway failure, falls back to real upstream directly.
5
6
  */
6
7
  import type { ProviderCredentials } from "./types.js";
7
8
  import type { PluginConfig } from "./types.js";
@@ -18,6 +19,10 @@ export declare class SidecarServer {
18
19
  private defaultProvider;
19
20
  private availableProviders;
20
21
  private options;
22
+ private cachedBaseUrlMap;
23
+ private cachedMemoryxApiKey;
24
+ private cachedAgentId;
25
+ private accountInfoResolved;
21
26
  constructor(credentials: Map<string, ProviderCredentials>, defaultProvider: {
22
27
  model: string;
23
28
  provider: string;
@@ -28,6 +33,14 @@ export declare class SidecarServer {
28
33
  start(): Promise<void>;
29
34
  stop(): Promise<void>;
30
35
  getPort(): number;
36
+ private warmCaches;
37
+ private getBaseUrlMap;
38
+ /**
39
+ * Fallback: forward request directly to the real upstream provider,
40
+ * bypassing the gateway. Used when gateway is unavailable.
41
+ * Uses the default provider's real base URL + the incoming API key.
42
+ */
43
+ private fallbackToUpstream;
31
44
  private handleRequest;
32
45
  private readBody;
33
46
  }
@@ -1 +1 @@
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;IA0BtB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAa3B,OAAO,IAAI,MAAM;YAMH,aAAa;IAiM3B,OAAO,CAAC,QAAQ;CAQnB"}
1
+ {"version":3,"file":"sidecar.d.ts","sourceRoot":"","sources":["../src/sidecar.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;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;IAEhC,OAAO,CAAC,gBAAgB,CAAuC;IAC/D,OAAO,CAAC,mBAAmB,CAAc;IACzC,OAAO,CAAC,aAAa,CAAc;IACnC,OAAO,CAAC,mBAAmB,CAAS;gBAGhC,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;IA2BtB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAa3B,OAAO,IAAI,MAAM;YAMH,UAAU;IAuBxB,OAAO,CAAC,aAAa;IAIrB;;;;OAIG;YACW,kBAAkB;YA6GlB,aAAa;IA8M3B,OAAO,CAAC,QAAQ;CAQnB"}
package/dist/sidecar.js CHANGED
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * Local HTTP Sidecar: lightweight forwarder.
3
- * Reads base_url_map from local cache file, credentials from constructor (api.config).
3
+ * Reads base_url_map from local cache file, credentials from constructor.
4
4
  * Packages payload and POSTs to MemoryX API server.
5
+ * On gateway failure, falls back to real upstream directly.
5
6
  */
6
7
  import * as http from "http";
7
8
  import { SIDECAR_PORT } from "./constants.js";
@@ -13,6 +14,10 @@ export class SidecarServer {
13
14
  defaultProvider;
14
15
  availableProviders;
15
16
  options;
17
+ cachedBaseUrlMap = null;
18
+ cachedMemoryxApiKey = "";
19
+ cachedAgentId = "";
20
+ accountInfoResolved = false;
16
21
  constructor(credentials, defaultProvider, availableProviders, options) {
17
22
  this.credentials = credentials;
18
23
  this.defaultProvider = defaultProvider;
@@ -20,6 +25,7 @@ export class SidecarServer {
20
25
  this.options = options;
21
26
  }
22
27
  async start() {
28
+ await this.warmCaches();
23
29
  return new Promise((resolve, reject) => {
24
30
  const server = http.createServer(async (req, res) => {
25
31
  await this.handleRequest(req, res);
@@ -63,17 +69,162 @@ export class SidecarServer {
63
69
  ? this.server.address().port
64
70
  : SIDECAR_PORT;
65
71
  }
72
+ async warmCaches() {
73
+ try {
74
+ const map = await loadRealUpstreamBaseUrlCache();
75
+ const obj = {};
76
+ for (const [id, u] of map) {
77
+ if (id && u?.trim())
78
+ obj[id] = u.trim();
79
+ }
80
+ this.cachedBaseUrlMap = obj;
81
+ }
82
+ catch {
83
+ this.cachedBaseUrlMap = {};
84
+ }
85
+ try {
86
+ const sdk = await this.options.getSDK(this.options.pluginConfig);
87
+ const accountInfo = await sdk.getAccountInfo();
88
+ this.cachedMemoryxApiKey = accountInfo.apiKey || "";
89
+ this.cachedAgentId = accountInfo.agentId || "";
90
+ this.accountInfoResolved = true;
91
+ }
92
+ catch {
93
+ this.accountInfoResolved = true;
94
+ }
95
+ }
96
+ getBaseUrlMap() {
97
+ return this.cachedBaseUrlMap ?? {};
98
+ }
99
+ /**
100
+ * Fallback: forward request directly to the real upstream provider,
101
+ * bypassing the gateway. Used when gateway is unavailable.
102
+ * Uses the default provider's real base URL + the incoming API key.
103
+ */
104
+ async fallbackToUpstream(reqUrl, res, reqId, parsedBody, requestFormat, incomingApiKey, anthropicVersion, provider) {
105
+ const baseUrlMap = this.getBaseUrlMap();
106
+ const realBaseUrl = baseUrlMap[provider] || "";
107
+ if (!realBaseUrl || !incomingApiKey) {
108
+ log(`[Sidecar] ${reqId} fallback: no upstream URL or key for ${provider}`, { console: true });
109
+ return false;
110
+ }
111
+ const fallbackBody = { ...parsedBody };
112
+ if (fallbackBody.model === "auto") {
113
+ const match = this.availableProviders.find(p => p.provider === provider);
114
+ if (match?.model)
115
+ fallbackBody.model = match.model;
116
+ }
117
+ const upstreamUrl = realBaseUrl.replace(/\/+$/, "") + reqUrl;
118
+ const headers = { "Content-Type": "application/json" };
119
+ if (requestFormat === "anthropic") {
120
+ headers["x-api-key"] = incomingApiKey;
121
+ headers["anthropic-version"] = anthropicVersion || "2023-06-01";
122
+ }
123
+ else {
124
+ headers["Authorization"] = `Bearer ${incomingApiKey}`;
125
+ }
126
+ log(`[Sidecar] ${reqId} fallback → ${provider} ${upstreamUrl} model=${fallbackBody.model} fmt=${requestFormat}`, { console: true });
127
+ const stream = !!fallbackBody.stream;
128
+ const timeout = stream ? 20_000 : 90_000;
129
+ const abort = new AbortController();
130
+ const timer = setTimeout(() => abort.abort(), timeout);
131
+ let resp;
132
+ try {
133
+ resp = await fetch(upstreamUrl, {
134
+ method: "POST",
135
+ headers,
136
+ body: JSON.stringify(fallbackBody),
137
+ signal: abort.signal,
138
+ });
139
+ }
140
+ catch (e) {
141
+ clearTimeout(timer);
142
+ log(`[Sidecar] ${reqId} fallback fetch error: ${e?.message}`, { console: true });
143
+ return false;
144
+ }
145
+ finally {
146
+ clearTimeout(timer);
147
+ }
148
+ if (resp.status >= 400) {
149
+ let detail = "";
150
+ try {
151
+ detail = await resp.text();
152
+ }
153
+ catch { }
154
+ log(`[Sidecar] ${reqId} fallback upstream ${resp.status}: ${detail.slice(0, 200)}`, { console: true });
155
+ return false;
156
+ }
157
+ if (stream && resp.body) {
158
+ const reader = resp.body.getReader();
159
+ res.writeHead(resp.status, {
160
+ "Content-Type": resp.headers.get("content-type") || "text/event-stream",
161
+ });
162
+ const FIRST_CHUNK_TIMEOUT = 120_000;
163
+ const CHUNK_TIMEOUT = 30_000;
164
+ let isFirst = true;
165
+ try {
166
+ while (true) {
167
+ const chunkTimeout = isFirst ? FIRST_CHUNK_TIMEOUT : CHUNK_TIMEOUT;
168
+ const { done, value } = await Promise.race([
169
+ reader.read(),
170
+ new Promise((_, rej) => setTimeout(() => rej(new Error("chunk timeout")), chunkTimeout)),
171
+ ]);
172
+ if (done || !value)
173
+ break;
174
+ if (res.destroyed)
175
+ break;
176
+ res.write(Buffer.from(value));
177
+ isFirst = false;
178
+ }
179
+ }
180
+ catch (e) {
181
+ log(`[Sidecar] ${reqId} fallback stream error: ${e?.message}`, { console: true });
182
+ }
183
+ finally {
184
+ try {
185
+ reader.cancel();
186
+ }
187
+ catch { }
188
+ try {
189
+ reader.releaseLock();
190
+ }
191
+ catch { }
192
+ }
193
+ res.end();
194
+ log(`[Sidecar] ${reqId} ← ${resp.status} fallback stream (${provider})`, { console: true });
195
+ }
196
+ else if (resp.body) {
197
+ const text = await resp.text();
198
+ res.writeHead(resp.status, {
199
+ "Content-Type": resp.headers.get("content-type") || "application/json",
200
+ });
201
+ res.end(text);
202
+ log(`[Sidecar] ${reqId} ← ${resp.status} fallback body=${text.length} (${provider})`, { console: true });
203
+ }
204
+ else {
205
+ res.writeHead(resp.status);
206
+ res.end();
207
+ }
208
+ return true;
209
+ }
66
210
  async handleRequest(req, res) {
67
- const url = req.url || "/";
211
+ let url = req.url || "/";
68
212
  const method = req.method?.toUpperCase();
69
- const { proxyUrl, getSDK, pluginConfig } = this.options;
213
+ const { proxyUrl } = this.options;
70
214
  if (url === "/health" && method === "GET") {
71
215
  res.writeHead(200, { "Content-Type": "text/plain" });
72
216
  res.end("OK");
73
217
  return;
74
218
  }
219
+ // Extract provider from URL path prefix: /_p/<provider>/chat/completions
220
+ let urlProvider = "";
221
+ const prefixMatch = url.match(/^\/_p\/([^/]+)(\/.*)/);
222
+ if (prefixMatch) {
223
+ urlProvider = prefixMatch[1];
224
+ url = prefixMatch[2]; // strip prefix, keep the real path
225
+ }
75
226
  const reqId = `req-${Date.now()}`;
76
- log(`[Sidecar] ${reqId} in ${method} ${url}`, { console: true });
227
+ log(`[Sidecar] ${reqId} in ${method} ${req.url}`, { console: true });
77
228
  let body;
78
229
  try {
79
230
  body = await this.readBody(req);
@@ -84,76 +235,60 @@ export class SidecarServer {
84
235
  res.end(JSON.stringify({ error: e?.message || "Read body failed" }));
85
236
  return;
86
237
  }
87
- let openaiRequest;
238
+ let parsedBody;
88
239
  try {
89
- openaiRequest = JSON.parse(body || "{}");
240
+ parsedBody = JSON.parse(body || "{}");
90
241
  }
91
242
  catch {
92
- openaiRequest = {};
243
+ parsedBody = {};
93
244
  }
94
- const stream = !!openaiRequest.stream;
95
- // Read base_url_map from cache file (async)
96
- let baseUrlMap;
97
- try {
98
- baseUrlMap = await loadRealUpstreamBaseUrlCache();
99
- }
100
- catch {
101
- baseUrlMap = new Map();
102
- }
103
- const baseUrlMapObj = {};
104
- for (const [id, u] of baseUrlMap) {
105
- if (id && u?.trim())
106
- baseUrlMapObj[id] = u.trim();
107
- }
108
- const rawProvider = req.headers["x-memoryx-real-provider"]?.trim() || "";
245
+ const stream = !!parsedBody.stream;
246
+ const isAnthropic = (url.includes("/messages") && !url.includes("/chat/completions"))
247
+ || !!req.headers["anthropic-version"];
248
+ const requestFormat = isAnthropic ? "anthropic" : "openai";
249
+ const baseUrlMapObj = this.getBaseUrlMap();
109
250
  const authHeader = req.headers["authorization"]?.trim() || "";
110
251
  const incomingApiKey = authHeader.startsWith("Bearer ")
111
252
  ? authHeader.slice(7).trim()
112
- : authHeader;
113
- // Build credentials: use incoming Authorization header as apiKey for the target provider
253
+ : req.headers["x-api-key"]?.trim() || authHeader;
254
+ const realProvider = urlProvider || this.defaultProvider.provider || "";
114
255
  const credentialsObj = {};
115
256
  for (const [id, creds] of this.credentials) {
116
257
  const realBaseUrl = baseUrlMapObj[id] || creds.baseUrl || "";
117
- const apiKey = (id === rawProvider && incomingApiKey)
118
- ? incomingApiKey
119
- : (creds.apiKey || "");
120
- credentialsObj[id] = { baseUrl: realBaseUrl, apiKey };
258
+ credentialsObj[id] = { baseUrl: realBaseUrl, apiKey: creds.apiKey || "" };
121
259
  }
122
- const messages = openaiRequest.messages || [];
260
+ const messages = parsedBody.messages || [];
123
261
  const lastUserMsg = [...messages].reverse().find((m) => m.role === "user");
124
262
  const searchQuery = typeof lastUserMsg?.content === "string" ? lastUserMsg.content : "";
125
- let memoryxApiKey = "";
126
- let agentId = "";
127
- try {
128
- const sdk = await getSDK(pluginConfig);
129
- const accountInfo = await sdk.getAccountInfo();
130
- memoryxApiKey = accountInfo.apiKey || "";
131
- agentId = accountInfo.agentId || "";
132
- }
133
- catch (_e) {
134
- /* do not block */
135
- }
136
263
  const proxyRequestBody = {
137
264
  base_url_map: baseUrlMapObj,
138
265
  credentials: credentialsObj,
139
266
  incoming_api_key: incomingApiKey || undefined,
140
- body: openaiRequest,
141
- request_headers: { "x-memoryx-real-provider": rawProvider },
267
+ body: parsedBody,
268
+ request_format: requestFormat,
269
+ request_headers: { "x-memoryx-real-provider": realProvider },
142
270
  search_query: searchQuery,
143
- agent_id: agentId,
271
+ agent_id: this.cachedAgentId,
144
272
  available_models: this.availableProviders.map((p) => `${p.provider}/${p.model}`),
145
273
  };
146
- const pathLog = (req.url || "/").split("?")[0] || "/";
147
- log(`[Sidecar] ${reqId} ${method} ${pathLog} model=${openaiRequest.model ?? "-"} stream=${stream} → proxy ${proxyUrl}`, { console: true });
148
- const FETCH_TIMEOUT = 75_000;
274
+ const pathLog = url.split("?")[0] || "/";
275
+ log(`[Sidecar] ${reqId} ${method} ${pathLog} fmt=${requestFormat} provider=${realProvider} model=${parsedBody.model ?? "-"} stream=${stream}`, { console: true });
276
+ const FETCH_TIMEOUT = stream ? 20_000 : 90_000;
277
+ const REASONING_CHUNK_TIMEOUT = 120_000;
149
278
  const CHUNK_TIMEOUT = 30_000;
150
279
  const fetchAbort = new AbortController();
151
280
  const fetchTimer = setTimeout(() => fetchAbort.abort(), FETCH_TIMEOUT);
281
+ req.on("close", () => {
282
+ if (!res.writableEnded) {
283
+ log(`[Sidecar] ${reqId} client closed connection, aborting upstream`, { console: true });
284
+ fetchAbort.abort();
285
+ }
286
+ });
152
287
  let proxyResponse = null;
153
288
  try {
154
289
  proxyResponse = await fetch(proxyUrl, {
155
290
  method: "POST",
156
- headers: { "Content-Type": "application/json", "X-API-Key": memoryxApiKey },
291
+ headers: { "Content-Type": "application/json", "X-API-Key": this.cachedMemoryxApiKey },
157
292
  body: JSON.stringify(proxyRequestBody),
158
293
  signal: fetchAbort.signal,
159
294
  });
@@ -180,34 +315,45 @@ export class SidecarServer {
180
315
  const status = proxyResponse?.status ?? 502;
181
316
  if (!detail)
182
317
  detail = proxyResponse?.statusText || "MemoryX proxy unavailable";
183
- log(`[Sidecar] ${reqId} ${status} fast-fail: ${detail.slice(0, 200)}`, { console: true });
318
+ log(`[Sidecar] ${reqId} proxy failed (${status}), trying upstream fallback…`, { console: true });
319
+ const fallbackOk = await this.fallbackToUpstream(url, res, reqId, parsedBody, requestFormat, incomingApiKey, req.headers["anthropic-version"] || "", realProvider);
320
+ if (fallbackOk)
321
+ return;
322
+ log(`[Sidecar] ${reqId} ← ${status} all routes failed: ${detail.slice(0, 200)}`, { console: true });
184
323
  res.writeHead(status, { "Content-Type": "application/json" });
185
324
  res.end(typeof detail === "string" && detail.startsWith("{") ? detail : JSON.stringify({ error: detail }));
186
325
  return;
187
326
  }
188
327
  const proxy = proxyResponse;
189
- if (openaiRequest.stream && proxy.body) {
328
+ if (parsedBody.stream && proxy.body) {
190
329
  const reader = proxy.body.getReader();
191
- const readWithTimeout = () => {
330
+ const readWithTimeout = (timeout) => {
192
331
  let timer;
193
332
  return Promise.race([
194
333
  reader.read().then((r) => { if (timer)
195
334
  clearTimeout(timer); return r; }),
196
335
  new Promise((_, rej) => {
197
- timer = setTimeout(() => rej(new Error("chunk timeout")), CHUNK_TIMEOUT);
336
+ timer = setTimeout(() => rej(new Error("chunk timeout")), timeout);
198
337
  }),
199
338
  ]);
200
339
  };
201
340
  let firstChunk;
202
341
  try {
203
- firstChunk = await readWithTimeout();
342
+ firstChunk = await readWithTimeout(REASONING_CHUNK_TIMEOUT);
204
343
  }
205
344
  catch (firstErr) {
345
+ try {
346
+ reader.cancel();
347
+ }
348
+ catch (_) { }
206
349
  try {
207
350
  reader.releaseLock();
208
351
  }
209
352
  catch (_) { }
210
- log(`[Sidecar] ${reqId} err stream first chunk: ${firstErr?.message ?? "timeout"}`, { console: true });
353
+ log(`[Sidecar] ${reqId} stream first chunk failed, trying upstream fallback…`, { console: true });
354
+ const fallbackOk = await this.fallbackToUpstream(url, res, reqId, parsedBody, requestFormat, incomingApiKey, req.headers["anthropic-version"] || "", realProvider);
355
+ if (fallbackOk)
356
+ return;
211
357
  res.writeHead(502, { "Content-Type": "application/json" });
212
358
  res.end(JSON.stringify({ error: "MemoryX proxy stream failed" }));
213
359
  return;
@@ -221,9 +367,10 @@ export class SidecarServer {
221
367
  while (true) {
222
368
  if (res.destroyed) {
223
369
  log(`[Sidecar] ${reqId} client disconnected, aborting stream`, { console: true });
370
+ fetchAbort.abort();
224
371
  break;
225
372
  }
226
- const { done, value } = await readWithTimeout();
373
+ const { done, value } = await readWithTimeout(CHUNK_TIMEOUT);
227
374
  if (done || !value)
228
375
  break;
229
376
  res.write(Buffer.from(value));
@@ -233,6 +380,10 @@ export class SidecarServer {
233
380
  log(`[Sidecar] ${reqId} stream error: ${streamErr?.message ?? "unknown"}`, { console: true });
234
381
  }
235
382
  finally {
383
+ try {
384
+ reader.cancel();
385
+ }
386
+ catch (_) { }
236
387
  try {
237
388
  reader.releaseLock();
238
389
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@t0ken.ai/memoryx-openclaw-plugin",
3
- "version": "2.2.68",
3
+ "version": "2.2.70",
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",