@t0ken.ai/memoryx-openclaw-plugin 2.2.69 → 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.69";
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.69";
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,CAiCN"}
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,5 +1,5 @@
1
+ import { log } from "./logger.js";
1
2
  export function registerHooks(api, plugin, wrapProvidersWithProxy, applySidecarRedirect, shouldApplyProxy) {
2
- let proxyApplied = false;
3
3
  api.on("message_received", async (event) => {
4
4
  const { content } = event;
5
5
  if (content && plugin)
@@ -13,12 +13,18 @@ export function registerHooks(api, plugin, wrapProvidersWithProxy, applySidecarR
13
13
  await plugin.onMessage("assistant", fullContent);
14
14
  }
15
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
+ });
16
27
  api.on("before_agent_start", async (_event) => {
17
- if (!proxyApplied && shouldApplyProxy()) {
18
- wrapProvidersWithProxy();
19
- applySidecarRedirect({ quiet: true });
20
- proxyApplied = true;
21
- }
22
28
  try {
23
29
  await plugin.startTimersIfNeeded();
24
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,wBAsGE;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,25 +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
- let cachedShouldProxy = null;
54
- const shouldApplyProxy = () => {
55
- if (cachedShouldProxy !== null)
56
- return cachedShouldProxy;
57
- const creds = extractProviderCredentials(api.config);
58
- cachedShouldProxy = getDefaultModelAndProvider(creds, api.config).provider === "memoryx-gateway";
59
- return cachedShouldProxy;
60
- };
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;
61
57
  registerHooks(api, plugin, wrapProvidersWithProxy, applySidecarRedirect, shouldApplyProxy);
62
58
  api.registerService({
63
59
  id: "memoryx-sidecar",
64
60
  start: async () => {
65
61
  try {
66
- // 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.
67
101
  const credentials = extractProviderCredentials(api.config);
68
102
  const defaults = getDefaultModelAndProvider(credentials, api.config);
69
- // Sync baseUrl cache: config → compare with cache → update non-localhost
70
- await syncRealUpstreamBaseUrlCacheFromConfig(api.config, getSidecarBase());
71
- const realUpstreamCredentials = realUpstreamCredentialsForSidecar(credentials, new Map());
103
+ const realUpstreamCredentials = realUpstreamCredentialsForSidecar(credentials, savedRealUpstreamUrls);
72
104
  sidecar = new SidecarServer(realUpstreamCredentials, { model: defaults.model, provider: defaults.provider }, defaults.availableProviders, {
73
105
  proxyUrl,
74
106
  getSDK,
@@ -77,16 +109,25 @@ export default {
77
109
  });
78
110
  await sidecar.start();
79
111
  api.logger.info(`[MemoryX] Sidecar started on port ${sidecar.getPort()}`);
80
- if (defaults.provider === "memoryx-gateway") {
81
- wrapProvidersWithProxy();
82
- applySidecarRedirect();
83
- }
84
112
  }
85
113
  catch (e) {
86
114
  api.logger.error(`[MemoryX] Sidecar failed to start: ${e.message}`);
87
115
  }
88
116
  },
89
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 */ }
90
131
  if (sidecar)
91
132
  await sidecar.stop();
92
133
  api.logger.info("[MemoryX] Sidecar stopped");
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";
@@ -34,6 +35,12 @@ export declare class SidecarServer {
34
35
  getPort(): number;
35
36
  private warmCaches;
36
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;
37
44
  private handleRequest;
38
45
  private readBody;
39
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;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;YAIP,aAAa;IAsL3B,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";
@@ -95,8 +96,119 @@ export class SidecarServer {
95
96
  getBaseUrlMap() {
96
97
  return this.cachedBaseUrlMap ?? {};
97
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
+ }
98
210
  async handleRequest(req, res) {
99
- const url = req.url || "/";
211
+ let url = req.url || "/";
100
212
  const method = req.method?.toUpperCase();
101
213
  const { proxyUrl } = this.options;
102
214
  if (url === "/health" && method === "GET") {
@@ -104,8 +216,15 @@ export class SidecarServer {
104
216
  res.end("OK");
105
217
  return;
106
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
+ }
107
226
  const reqId = `req-${Date.now()}`;
108
- log(`[Sidecar] ${reqId} in ${method} ${url}`, { console: true });
227
+ log(`[Sidecar] ${reqId} in ${method} ${req.url}`, { console: true });
109
228
  let body;
110
229
  try {
111
230
  body = await this.readBody(req);
@@ -116,43 +235,44 @@ export class SidecarServer {
116
235
  res.end(JSON.stringify({ error: e?.message || "Read body failed" }));
117
236
  return;
118
237
  }
119
- let openaiRequest;
238
+ let parsedBody;
120
239
  try {
121
- openaiRequest = JSON.parse(body || "{}");
240
+ parsedBody = JSON.parse(body || "{}");
122
241
  }
123
242
  catch {
124
- openaiRequest = {};
243
+ parsedBody = {};
125
244
  }
126
- const stream = !!openaiRequest.stream;
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";
127
249
  const baseUrlMapObj = this.getBaseUrlMap();
128
- const rawProvider = req.headers["x-memoryx-real-provider"]?.trim() || "";
129
250
  const authHeader = req.headers["authorization"]?.trim() || "";
130
251
  const incomingApiKey = authHeader.startsWith("Bearer ")
131
252
  ? authHeader.slice(7).trim()
132
- : authHeader;
253
+ : req.headers["x-api-key"]?.trim() || authHeader;
254
+ const realProvider = urlProvider || this.defaultProvider.provider || "";
133
255
  const credentialsObj = {};
134
256
  for (const [id, creds] of this.credentials) {
135
257
  const realBaseUrl = baseUrlMapObj[id] || creds.baseUrl || "";
136
- const apiKey = (id === rawProvider && incomingApiKey)
137
- ? incomingApiKey
138
- : (creds.apiKey || "");
139
- credentialsObj[id] = { baseUrl: realBaseUrl, apiKey };
258
+ credentialsObj[id] = { baseUrl: realBaseUrl, apiKey: creds.apiKey || "" };
140
259
  }
141
- const messages = openaiRequest.messages || [];
260
+ const messages = parsedBody.messages || [];
142
261
  const lastUserMsg = [...messages].reverse().find((m) => m.role === "user");
143
262
  const searchQuery = typeof lastUserMsg?.content === "string" ? lastUserMsg.content : "";
144
263
  const proxyRequestBody = {
145
264
  base_url_map: baseUrlMapObj,
146
265
  credentials: credentialsObj,
147
266
  incoming_api_key: incomingApiKey || undefined,
148
- body: openaiRequest,
149
- request_headers: { "x-memoryx-real-provider": rawProvider },
267
+ body: parsedBody,
268
+ request_format: requestFormat,
269
+ request_headers: { "x-memoryx-real-provider": realProvider },
150
270
  search_query: searchQuery,
151
271
  agent_id: this.cachedAgentId,
152
272
  available_models: this.availableProviders.map((p) => `${p.provider}/${p.model}`),
153
273
  };
154
- const pathLog = (req.url || "/").split("?")[0] || "/";
155
- log(`[Sidecar] ${reqId} ${method} ${pathLog} model=${openaiRequest.model ?? "-"} stream=${stream} → proxy ${proxyUrl}`, { console: true });
274
+ const pathLog = url.split("?")[0] || "/";
275
+ log(`[Sidecar] ${reqId} ${method} ${pathLog} fmt=${requestFormat} provider=${realProvider} model=${parsedBody.model ?? "-"} stream=${stream}`, { console: true });
156
276
  const FETCH_TIMEOUT = stream ? 20_000 : 90_000;
157
277
  const REASONING_CHUNK_TIMEOUT = 120_000;
158
278
  const CHUNK_TIMEOUT = 30_000;
@@ -195,13 +315,17 @@ export class SidecarServer {
195
315
  const status = proxyResponse?.status ?? 502;
196
316
  if (!detail)
197
317
  detail = proxyResponse?.statusText || "MemoryX proxy unavailable";
198
- 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 });
199
323
  res.writeHead(status, { "Content-Type": "application/json" });
200
324
  res.end(typeof detail === "string" && detail.startsWith("{") ? detail : JSON.stringify({ error: detail }));
201
325
  return;
202
326
  }
203
327
  const proxy = proxyResponse;
204
- if (openaiRequest.stream && proxy.body) {
328
+ if (parsedBody.stream && proxy.body) {
205
329
  const reader = proxy.body.getReader();
206
330
  const readWithTimeout = (timeout) => {
207
331
  let timer;
@@ -226,7 +350,10 @@ export class SidecarServer {
226
350
  reader.releaseLock();
227
351
  }
228
352
  catch (_) { }
229
- 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;
230
357
  res.writeHead(502, { "Content-Type": "application/json" });
231
358
  res.end(JSON.stringify({ error: "MemoryX proxy stream failed" }));
232
359
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@t0ken.ai/memoryx-openclaw-plugin",
3
- "version": "2.2.69",
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",