@t0ken.ai/memoryx-openclaw-plugin 2.2.69 → 2.2.71
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -1
- package/dist/hooks.d.ts +2 -1
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +13 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +59 -18
- package/dist/sidecar.d.ts +8 -1
- package/dist/sidecar.d.ts.map +1 -1
- package/dist/sidecar.js +148 -21
- package/package.json +1 -1
package/dist/constants.d.ts
CHANGED
package/dist/constants.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import * as path from "path";
|
|
5
5
|
import * as os from "os";
|
|
6
6
|
// Plugin version - synced from package.json by prebuild script
|
|
7
|
-
export const PLUGIN_VERSION = "2.2.
|
|
7
|
+
export const PLUGIN_VERSION = "2.2.71";
|
|
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,
|
|
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?: {
|
package/dist/hooks.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../src/hooks.ts"],"names":[],"mappings":"AAAA
|
|
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,CAqCN"}
|
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,19 @@ export function registerHooks(api, plugin, wrapProvidersWithProxy, applySidecarR
|
|
|
13
13
|
await plugin.onMessage("assistant", fullContent);
|
|
14
14
|
}
|
|
15
15
|
});
|
|
16
|
+
// Don't override the model — let OpenClaw resolve the real model normally.
|
|
17
|
+
// The provider's baseUrl is patched to the sidecar in service.start
|
|
18
|
+
// (runtimeConfigSnapshot), so requests route through:
|
|
19
|
+
// sidecar → gateway → actual provider.
|
|
20
|
+
// Overriding to "auto" broke after config hot-reloads reset the baseUrl
|
|
21
|
+
// patch, causing "auto" to hit the real API (which rejects it as invalid).
|
|
22
|
+
api.on("before_model_resolve", async () => {
|
|
23
|
+
if (!shouldApplyProxy())
|
|
24
|
+
return undefined;
|
|
25
|
+
log(`[Proxy] before_model_resolve → passthrough (baseUrl redirect active)`);
|
|
26
|
+
return undefined;
|
|
27
|
+
});
|
|
16
28
|
api.on("before_agent_start", async (_event) => {
|
|
17
|
-
if (!proxyApplied && shouldApplyProxy()) {
|
|
18
|
-
wrapProvidersWithProxy();
|
|
19
|
-
applySidecarRedirect({ quiet: true });
|
|
20
|
-
proxyApplied = true;
|
|
21
|
-
}
|
|
22
29
|
try {
|
|
23
30
|
await plugin.startTimersIfNeeded();
|
|
24
31
|
}
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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;;;;;;
|
|
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,
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|
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
|
}
|
package/dist/sidecar.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sidecar.d.ts","sourceRoot":"","sources":["../src/sidecar.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"sidecar.d.ts","sourceRoot":"","sources":["../src/sidecar.ts"],"names":[],"mappings":"AAAA;;;;;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
|
|
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
|
-
|
|
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
|
|
238
|
+
let parsedBody;
|
|
120
239
|
try {
|
|
121
|
-
|
|
240
|
+
parsedBody = JSON.parse(body || "{}");
|
|
122
241
|
}
|
|
123
242
|
catch {
|
|
124
|
-
|
|
243
|
+
parsedBody = {};
|
|
125
244
|
}
|
|
126
|
-
const 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
|
-
|
|
137
|
-
? incomingApiKey
|
|
138
|
-
: (creds.apiKey || "");
|
|
139
|
-
credentialsObj[id] = { baseUrl: realBaseUrl, apiKey };
|
|
258
|
+
credentialsObj[id] = { baseUrl: realBaseUrl, apiKey: creds.apiKey || "" };
|
|
140
259
|
}
|
|
141
|
-
const 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:
|
|
149
|
-
|
|
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 =
|
|
155
|
-
log(`[Sidecar] ${reqId} ${method} ${pathLog} model=${
|
|
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}
|
|
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 (
|
|
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}
|
|
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