@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.
- 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 +12 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +59 -14
- package/dist/proxy-redirect.d.ts.map +1 -1
- package/dist/proxy-redirect.js +46 -27
- package/dist/sidecar.d.ts +14 -1
- package/dist/sidecar.d.ts.map +1 -1
- package/dist/sidecar.js +205 -54
- 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.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,
|
|
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,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
|
}
|
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,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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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":"
|
|
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"}
|
package/dist/proxy-redirect.js
CHANGED
|
@@ -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 =
|
|
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
|
|
49
|
-
|
|
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
|
|
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(
|
|
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
|
|
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
|
}
|
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";
|
|
@@ -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
|
-
|
|
211
|
+
let url = req.url || "/";
|
|
68
212
|
const method = req.method?.toUpperCase();
|
|
69
|
-
const { proxyUrl
|
|
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
|
|
238
|
+
let parsedBody;
|
|
88
239
|
try {
|
|
89
|
-
|
|
240
|
+
parsedBody = JSON.parse(body || "{}");
|
|
90
241
|
}
|
|
91
242
|
catch {
|
|
92
|
-
|
|
243
|
+
parsedBody = {};
|
|
93
244
|
}
|
|
94
|
-
const stream = !!
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
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
|
-
|
|
118
|
-
? incomingApiKey
|
|
119
|
-
: (creds.apiKey || "");
|
|
120
|
-
credentialsObj[id] = { baseUrl: realBaseUrl, apiKey };
|
|
258
|
+
credentialsObj[id] = { baseUrl: realBaseUrl, apiKey: creds.apiKey || "" };
|
|
121
259
|
}
|
|
122
|
-
const 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:
|
|
141
|
-
|
|
267
|
+
body: parsedBody,
|
|
268
|
+
request_format: requestFormat,
|
|
269
|
+
request_headers: { "x-memoryx-real-provider": realProvider },
|
|
142
270
|
search_query: searchQuery,
|
|
143
|
-
agent_id:
|
|
271
|
+
agent_id: this.cachedAgentId,
|
|
144
272
|
available_models: this.availableProviders.map((p) => `${p.provider}/${p.model}`),
|
|
145
273
|
};
|
|
146
|
-
const pathLog =
|
|
147
|
-
log(`[Sidecar] ${reqId} ${method} ${pathLog} model=${
|
|
148
|
-
const FETCH_TIMEOUT =
|
|
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":
|
|
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}
|
|
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 (
|
|
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")),
|
|
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}
|
|
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