@t0ken.ai/memoryx-openclaw-plugin 2.2.64 → 2.2.66
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 +1 -1
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +2 -5
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +33 -31
- package/dist/proxy-credentials.d.ts +16 -41
- package/dist/proxy-credentials.d.ts.map +1 -1
- package/dist/proxy-credentials.js +33 -108
- package/dist/proxy-redirect.d.ts.map +1 -1
- package/dist/proxy-redirect.js +29 -22
- package/dist/sidecar.d.ts +3 -3
- package/dist/sidecar.d.ts.map +1 -1
- package/dist/sidecar.js +53 -24
- 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.66";
|
|
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
|
@@ -4,5 +4,5 @@
|
|
|
4
4
|
import type { MemoryXPlugin } from "./plugin-core.js";
|
|
5
5
|
export declare function registerHooks(api: any, plugin: MemoryXPlugin, wrapProvidersWithProxy: () => void, applySidecarRedirect: (opts?: {
|
|
6
6
|
quiet?: boolean;
|
|
7
|
-
}) => void, shouldApplyProxy: () => boolean
|
|
7
|
+
}) => void, shouldApplyProxy: () => boolean): void;
|
|
8
8
|
//# sourceMappingURL=hooks.d.ts.map
|
package/dist/hooks.d.ts.map
CHANGED
|
@@ -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,
|
|
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"}
|
package/dist/hooks.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export function registerHooks(api, plugin, wrapProvidersWithProxy, applySidecarRedirect, shouldApplyProxy
|
|
1
|
+
export function registerHooks(api, plugin, wrapProvidersWithProxy, applySidecarRedirect, shouldApplyProxy) {
|
|
2
2
|
let useVirtualProviderInLastRun = false;
|
|
3
3
|
api.on("message_received", async (event) => {
|
|
4
4
|
const { content } = event;
|
|
@@ -16,16 +16,13 @@ export function registerHooks(api, plugin, wrapProvidersWithProxy, applySidecarR
|
|
|
16
16
|
await plugin.onMessage("assistant", fullContent);
|
|
17
17
|
}
|
|
18
18
|
});
|
|
19
|
-
api.on("before_agent_start", async (
|
|
19
|
+
api.on("before_agent_start", async (_event) => {
|
|
20
20
|
if (shouldApplyProxy()) {
|
|
21
|
-
await syncRealUpstreamBaseUrlCache?.();
|
|
22
21
|
wrapProvidersWithProxy();
|
|
23
22
|
applySidecarRedirect({ quiet: true });
|
|
24
23
|
}
|
|
25
24
|
try {
|
|
26
25
|
await plugin.startTimersIfNeeded();
|
|
27
|
-
// 用户消息只由 message_received 写入一次,此处不再 onMessage("user", prompt),
|
|
28
|
-
// 避免与 message_received 重复,且 before_agent_start 可能被多次触发(如 tool 多轮)导致同条 prompt 重复入队。
|
|
29
26
|
}
|
|
30
27
|
catch (error) {
|
|
31
28
|
api.logger.warn(`[MemoryX] before_agent_start failed: ${error}`);
|
package/dist/index.d.ts
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
* - All initialization (DB, config loading, etc.) must be lazy-loaded
|
|
10
10
|
* - Use setImmediate() for deferred operations
|
|
11
11
|
*
|
|
12
|
+
* All file I/O (baseUrl cache) happens in service start (async).
|
|
13
|
+
*
|
|
12
14
|
* This version uses @t0ken.ai/memoryx-sdk with conversation preset:
|
|
13
15
|
* - maxTokens: 30000 (flush when reaching token limit)
|
|
14
16
|
* - intervalMs: 300000 (flush after 5 minutes idle)
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA
|
|
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"}
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
* - All initialization (DB, config loading, etc.) must be lazy-loaded
|
|
10
10
|
* - Use setImmediate() for deferred operations
|
|
11
11
|
*
|
|
12
|
+
* All file I/O (baseUrl cache) happens in service start (async).
|
|
13
|
+
*
|
|
12
14
|
* This version uses @t0ken.ai/memoryx-sdk with conversation preset:
|
|
13
15
|
* - maxTokens: 30000 (flush when reaching token limit)
|
|
14
16
|
* - intervalMs: 300000 (flush after 5 minutes idle)
|
|
@@ -37,50 +39,56 @@ export default {
|
|
|
37
39
|
}
|
|
38
40
|
plugin = new MemoryXPlugin(pluginConfig);
|
|
39
41
|
registerTools(api, plugin);
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
42
|
+
// Pure memory: read provider list from api.config (no file I/O, same as slimclaw)
|
|
43
|
+
const configCredentials = extractProviderCredentials(api.config);
|
|
44
|
+
log(`[Proxy] Found ${configCredentials.size} providers in config`);
|
|
45
|
+
const defaultConfig = getDefaultModelAndProvider(configCredentials, api.config);
|
|
43
46
|
log(`[Proxy] Default: ${defaultConfig.provider}/${defaultConfig.model}`);
|
|
44
47
|
const realProviderHeader = "X-MemoryX-Real-Provider";
|
|
45
48
|
const sidecarBaseUrl = `http://localhost:${SIDECAR_PORT}`;
|
|
46
|
-
const realUpstreamCredentials = realUpstreamCredentialsForSidecar(providerCredentials, new Map());
|
|
47
49
|
const proxyUrl = (pluginConfig?.apiBaseUrl || DEFAULT_API_BASE) + "/llm/proxy/chat/completions";
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
getSDK,
|
|
51
|
-
pluginConfig,
|
|
52
|
-
onStarted: (logFile) => api.logger.info(`[MemoryX] Plugin log file: ${logFile}`),
|
|
53
|
-
});
|
|
54
|
-
const getSidecarBase = () => `http://localhost:${sidecar.getPort()}`;
|
|
50
|
+
let sidecar = null;
|
|
51
|
+
const getSidecarBase = () => sidecar ? `http://localhost:${sidecar.getPort()}` : sidecarBaseUrl;
|
|
55
52
|
const { applySidecarRedirect, wrapProvidersWithProxy } = createProxyRedirect(api, getSidecarBase, realProviderHeader);
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
53
|
+
const shouldApplyProxy = () => {
|
|
54
|
+
const creds = extractProviderCredentials(api.config);
|
|
55
|
+
return getDefaultModelAndProvider(creds, api.config).provider === "memoryx-gateway";
|
|
56
|
+
};
|
|
57
|
+
registerHooks(api, plugin, wrapProvidersWithProxy, applySidecarRedirect, shouldApplyProxy);
|
|
60
58
|
api.registerService({
|
|
61
59
|
id: "memoryx-sidecar",
|
|
62
60
|
start: async () => {
|
|
63
61
|
try {
|
|
62
|
+
// Re-read credentials from api.config (pure memory, may have updated since register)
|
|
63
|
+
const credentials = extractProviderCredentials(api.config);
|
|
64
|
+
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());
|
|
68
|
+
sidecar = new SidecarServer(realUpstreamCredentials, { model: defaults.model, provider: defaults.provider }, defaults.availableProviders, {
|
|
69
|
+
proxyUrl,
|
|
70
|
+
getSDK,
|
|
71
|
+
pluginConfig,
|
|
72
|
+
onStarted: (logFile) => api.logger.info(`[MemoryX] Plugin log file: ${logFile}`),
|
|
73
|
+
});
|
|
64
74
|
await sidecar.start();
|
|
65
75
|
api.logger.info(`[MemoryX] Sidecar started on port ${sidecar.getPort()}`);
|
|
76
|
+
if (defaults.provider === "memoryx-gateway") {
|
|
77
|
+
wrapProvidersWithProxy();
|
|
78
|
+
applySidecarRedirect();
|
|
79
|
+
}
|
|
66
80
|
}
|
|
67
81
|
catch (e) {
|
|
68
82
|
api.logger.error(`[MemoryX] Sidecar failed to start: ${e.message}`);
|
|
69
83
|
}
|
|
70
84
|
},
|
|
71
85
|
stop: async () => {
|
|
72
|
-
|
|
86
|
+
if (sidecar)
|
|
87
|
+
await sidecar.stop();
|
|
73
88
|
api.logger.info("[MemoryX] Sidecar stopped");
|
|
74
89
|
},
|
|
75
90
|
});
|
|
76
|
-
|
|
77
|
-
wrapProvidersWithProxy();
|
|
78
|
-
applySidecarRedirect();
|
|
79
|
-
api.logger.info(`[MemoryX] ✅ Plugin v${PLUGIN_VERSION} ready! Default is MemoryX Gateway, redirect applied.`);
|
|
80
|
-
}
|
|
81
|
-
else {
|
|
82
|
-
api.logger.info(`[MemoryX] ✅ Plugin v${PLUGIN_VERSION} ready! Default is ${defaultConfig.provider}/${defaultConfig.model}; switch to MemoryX Gateway to use MemoryX.`);
|
|
83
|
-
}
|
|
91
|
+
api.logger.info(`[MemoryX] ✅ Plugin v${PLUGIN_VERSION} registered (I/O deferred to service start).`);
|
|
84
92
|
setImmediate(async () => {
|
|
85
93
|
try {
|
|
86
94
|
await plugin.init();
|
|
@@ -89,13 +97,7 @@ export default {
|
|
|
89
97
|
api.logger.info(`[MemoryX] 💡 Manage your memories: https://t0ken.ai/portal?api_key=${info.apiKey}`);
|
|
90
98
|
}
|
|
91
99
|
}
|
|
92
|
-
catch (
|
|
93
|
-
// ignore
|
|
94
|
-
}
|
|
95
|
-
try {
|
|
96
|
-
await syncRealUpstreamBaseUrlCacheFromConfig(api.config, sidecarBaseUrl);
|
|
97
|
-
}
|
|
98
|
-
catch (e) {
|
|
100
|
+
catch (_) {
|
|
99
101
|
// ignore
|
|
100
102
|
}
|
|
101
103
|
});
|
|
@@ -1,67 +1,42 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Extract and resolve provider credentials from OpenClaw config
|
|
2
|
+
* Extract and resolve provider credentials from OpenClaw config (api.config).
|
|
3
3
|
*
|
|
4
|
-
* API key
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
4
|
+
* API key resolution order (same as slimclaw):
|
|
5
|
+
* 1. providerOverrides[id].apiKey (explicit)
|
|
6
|
+
* 2. api.config.models.providers[id].apiKey (openclaw.json)
|
|
7
|
+
* 3. providerOverrides[id].apiKeyEnv → process.env[name]
|
|
8
|
+
* 4. process.env[${PROVIDER_ID}_API_KEY]
|
|
9
|
+
*
|
|
10
|
+
* NO file I/O in this module's credential extraction.
|
|
11
|
+
* Only real-upstream-baseurl.json cache uses async file I/O.
|
|
11
12
|
*/
|
|
12
13
|
import type { ProviderCredentials } from "./types.js";
|
|
14
|
+
/**
|
|
15
|
+
* Extract provider credentials from api.config only.
|
|
16
|
+
* Follows slimclaw pattern: config.models.providers + env vars, no system file access.
|
|
17
|
+
*/
|
|
13
18
|
export declare function extractProviderCredentials(config: any, providerOverrides?: Record<string, {
|
|
14
19
|
baseUrl?: string;
|
|
15
20
|
apiKeyEnv?: string;
|
|
16
21
|
apiKey?: string;
|
|
17
22
|
}>): Map<string, ProviderCredentials>;
|
|
18
|
-
/**
|
|
19
|
-
* 在 init 时从 OpenClaw 配置(api.config)读到的「所有厂商 -> baseUrl」做一次快照,不写死任何厂商地址。
|
|
20
|
-
* - 来源唯一:插件启动时通过 api 读到的 openclaw 配置文件(openclaw.json 等)里的 models.providers[id].baseUrl。
|
|
21
|
-
* - 若某条已是 localhost(例如上次 redirect 被持久化),本次仍写入 localhost;恢复依赖 setImmediate 里从缓存文件合并(首次正常时已落盘)。
|
|
22
|
-
* 返回 Map<providerId, baseUrl>,memoryx-gateway 不写入。
|
|
23
|
-
*/
|
|
24
23
|
export declare function buildRealUpstreamBaseUrlMap(credentials: Map<string, ProviderCredentials>, _sidecarBaseUrl: string): Map<string, string>;
|
|
25
|
-
/**
|
|
26
|
-
* 用 init 时缓存的「真实上游 baseUrl 映射表」生成供 Sidecar 使用的 credentials。
|
|
27
|
-
* 发给 llm_proxy 的 base_url_map(各厂商 api_base)均来自此映射表(真实 baseUrl),不会被 redirect 后的 localhost 覆盖。
|
|
28
|
-
* 用户改配置后重启即可生效。
|
|
29
|
-
*/
|
|
30
24
|
export declare function realUpstreamCredentialsForSidecar(credentials: Map<string, ProviderCredentials>, realUpstreamBaseUrlMap: Map<string, string>): Map<string, ProviderCredentials>;
|
|
31
|
-
/**
|
|
32
|
-
* 从缓存文件加载「厂商 -> 真实 baseUrl」映射。文件不存在或解析失败返回空 Map。
|
|
33
|
-
* 供插件 sync 时使用(先读已有缓存,没有则当空)。
|
|
34
|
-
*/
|
|
35
25
|
export declare function loadRealUpstreamBaseUrlCache(): Promise<Map<string, string>>;
|
|
26
|
+
export declare function saveRealUpstreamBaseUrlCache(map: Map<string, string>): Promise<void>;
|
|
27
|
+
export declare function isLocalhostBaseUrl(url: string, sidecarBaseUrl: string): boolean;
|
|
36
28
|
/**
|
|
37
|
-
*
|
|
38
|
-
* 供 Sidecar 每次请求读盘使用,不缓存。
|
|
39
|
-
*/
|
|
40
|
-
export declare function loadRealUpstreamBaseUrlCacheStrict(): Promise<Map<string, string>>;
|
|
41
|
-
/**
|
|
42
|
-
* 根据 OpenClaw 配置更新 real-upstream-baseurl.json:非 localhost 的 baseUrl 写入/覆盖,localhost 绝不覆盖。
|
|
29
|
+
* Sync real-upstream-baseurl.json: config baseUrl vs cache, update non-localhost entries.
|
|
43
30
|
*/
|
|
44
31
|
export declare function syncRealUpstreamBaseUrlCacheFromConfig(config: any, sidecarBaseUrl: string): Promise<void>;
|
|
45
|
-
/**
|
|
46
|
-
* 将「厂商 -> 真实 baseUrl」映射写入缓存文件,重启后若配置被改成 localhost 可从此恢复。
|
|
47
|
-
*/
|
|
48
|
-
export declare function saveRealUpstreamBaseUrlCache(map: Map<string, string>): Promise<void>;
|
|
49
|
-
/** 判断是否为 localhost(或等于 sidecarBaseUrl),用于合并缓存时决定是否用缓存值覆盖。 */
|
|
50
|
-
export declare function isLocalhostBaseUrl(url: string, sidecarBaseUrl: string): boolean;
|
|
51
|
-
/** Get all available provider/model list (for server fallback/retry); only providers with apiKey set */
|
|
52
32
|
export declare function getAvailableProviders(credentials: Map<string, ProviderCredentials>): Array<{
|
|
53
33
|
provider: string;
|
|
54
34
|
model: string;
|
|
55
35
|
}>;
|
|
56
|
-
/**
|
|
57
|
-
* Default primary: one model per provider (first listed); only providers with apiKey set.
|
|
58
|
-
* Order follows config.models.providers key order (Map insertion order).
|
|
59
|
-
*/
|
|
60
36
|
export declare function getDefaultProviderModelList(credentials: Map<string, ProviderCredentials>): Array<{
|
|
61
37
|
provider: string;
|
|
62
38
|
model: string;
|
|
63
39
|
}>;
|
|
64
|
-
/** Get default model/provider and full availableProviders; prefer config.agents.defaults.model.primary, else first in list. */
|
|
65
40
|
export declare function getDefaultModelAndProvider(credentials: Map<string, ProviderCredentials>, config?: any): {
|
|
66
41
|
model: string;
|
|
67
42
|
provider: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"proxy-credentials.d.ts","sourceRoot":"","sources":["../src/proxy-credentials.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"proxy-credentials.d.ts","sourceRoot":"","sources":["../src/proxy-credentials.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAStD;;;GAGG;AACH,wBAAgB,0BAA0B,CACtC,MAAM,EAAE,GAAG,EACX,iBAAiB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,GAC9F,GAAG,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAuClC;AAMD,wBAAgB,2BAA2B,CACvC,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,mBAAmB,CAAC,EAC7C,eAAe,EAAE,MAAM,GACxB,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAQrB;AAED,wBAAgB,iCAAiC,CAC7C,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,mBAAmB,CAAC,EAC7C,sBAAsB,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAC5C,GAAG,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAWlC;AAED,wBAAsB,4BAA4B,IAAI,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAcjF;AAED,wBAAsB,4BAA4B,CAAC,GAAG,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAgB1F;AAED,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAI/E;AAED;;GAEG;AACH,wBAAsB,sCAAsC,CACxD,MAAM,EAAE,GAAG,EACX,cAAc,EAAE,MAAM,GACvB,OAAO,CAAC,IAAI,CAAC,CAiBf;AAMD,wBAAgB,qBAAqB,CACjC,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,mBAAmB,CAAC,GAC9C,KAAK,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAa5C;AAED,wBAAgB,2BAA2B,CACvC,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,mBAAmB,CAAC,GAC9C,KAAK,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAS5C;AAmBD,wBAAgB,0BAA0B,CACtC,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,mBAAmB,CAAC,EAC7C,MAAM,CAAC,EAAE,GAAG,GACb;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,kBAAkB,EAAE,KAAK,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CAAE,CA8BrG"}
|
|
@@ -1,40 +1,15 @@
|
|
|
1
1
|
import { log } from "./logger.js";
|
|
2
2
|
import { PLUGIN_DIR, REAL_UPSTREAM_BASEURL_CACHE_FILE } from "./constants.js";
|
|
3
3
|
import * as fs from "fs";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
if (!fs.existsSync(profilesPath))
|
|
12
|
-
return out;
|
|
13
|
-
const raw = fs.readFileSync(profilesPath, "utf8");
|
|
14
|
-
const data = JSON.parse(raw);
|
|
15
|
-
const profiles = data?.profiles ?? {};
|
|
16
|
-
for (const [profileId, p] of Object.entries(profiles)) {
|
|
17
|
-
if (!p || typeof p !== "object")
|
|
18
|
-
continue;
|
|
19
|
-
const isApiKey = (p.type === "api_key" || p.mode === "api_key");
|
|
20
|
-
const value = (p.key ?? p.token ?? "")?.trim();
|
|
21
|
-
if (!isApiKey || !value)
|
|
22
|
-
continue;
|
|
23
|
-
const providerId = profileId.includes(":") ? profileId.split(":")[0] : profileId;
|
|
24
|
-
if (!out.has(providerId))
|
|
25
|
-
out.set(providerId, value);
|
|
26
|
-
if (p.provider && p.provider !== providerId && !out.has(p.provider))
|
|
27
|
-
out.set(p.provider, value);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
catch (_) {
|
|
31
|
-
// ignore
|
|
32
|
-
}
|
|
33
|
-
return out;
|
|
34
|
-
}
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Provider credentials from api.config (pure memory, no file I/O)
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
/**
|
|
8
|
+
* Extract provider credentials from api.config only.
|
|
9
|
+
* Follows slimclaw pattern: config.models.providers + env vars, no system file access.
|
|
10
|
+
*/
|
|
35
11
|
export function extractProviderCredentials(config, providerOverrides) {
|
|
36
12
|
const credentials = new Map();
|
|
37
|
-
const authKeys = loadAuthProfilesKeys(DEFAULT_AUTH_PROFILES_PATH);
|
|
38
13
|
if (config?.models?.providers) {
|
|
39
14
|
for (const [id, providerConfig] of Object.entries(config.models.providers)) {
|
|
40
15
|
if (typeof providerConfig !== "object" ||
|
|
@@ -51,7 +26,6 @@ export function extractProviderCredentials(config, providerOverrides) {
|
|
|
51
26
|
(typeof pc.apiKey === "string" ? pc.apiKey.trim() : "") ||
|
|
52
27
|
(customEnvName ? process.env[customEnvName] : undefined) ||
|
|
53
28
|
process.env[`${id.toUpperCase()}_API_KEY`] ||
|
|
54
|
-
authKeys.get(id) ||
|
|
55
29
|
"";
|
|
56
30
|
const isMemoryxGateway = (id === "memoryx-gateway");
|
|
57
31
|
credentials.set(id, {
|
|
@@ -63,21 +37,13 @@ export function extractProviderCredentials(config, providerOverrides) {
|
|
|
63
37
|
}
|
|
64
38
|
}
|
|
65
39
|
if (credentials.size > 0) {
|
|
66
|
-
|
|
67
|
-
const authFromProfiles = [...credentials.keys()].filter((id) => authKeys.has(id));
|
|
68
|
-
const summary = authFromProfiles.length > 0
|
|
69
|
-
? `[Proxy] Credentials: ${parts.join(", ")}; apiKey from auth-profiles: ${authFromProfiles.join(", ")}`
|
|
70
|
-
: `[Proxy] Credentials: ${parts.join(", ")}`;
|
|
71
|
-
log(summary);
|
|
40
|
+
log(`[Proxy] Providers: ${[...credentials.keys()].join(", ")}`);
|
|
72
41
|
}
|
|
73
42
|
return credentials;
|
|
74
43
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
* - 若某条已是 localhost(例如上次 redirect 被持久化),本次仍写入 localhost;恢复依赖 setImmediate 里从缓存文件合并(首次正常时已落盘)。
|
|
79
|
-
* 返回 Map<providerId, baseUrl>,memoryx-gateway 不写入。
|
|
80
|
-
*/
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// baseUrl cache (real-upstream-baseurl.json) — async file I/O only
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
81
47
|
export function buildRealUpstreamBaseUrlMap(credentials, _sidecarBaseUrl) {
|
|
82
48
|
const map = new Map();
|
|
83
49
|
for (const [id, creds] of credentials) {
|
|
@@ -88,11 +54,6 @@ export function buildRealUpstreamBaseUrlMap(credentials, _sidecarBaseUrl) {
|
|
|
88
54
|
}
|
|
89
55
|
return map;
|
|
90
56
|
}
|
|
91
|
-
/**
|
|
92
|
-
* 用 init 时缓存的「真实上游 baseUrl 映射表」生成供 Sidecar 使用的 credentials。
|
|
93
|
-
* 发给 llm_proxy 的 base_url_map(各厂商 api_base)均来自此映射表(真实 baseUrl),不会被 redirect 后的 localhost 覆盖。
|
|
94
|
-
* 用户改配置后重启即可生效。
|
|
95
|
-
*/
|
|
96
57
|
export function realUpstreamCredentialsForSidecar(credentials, realUpstreamBaseUrlMap) {
|
|
97
58
|
const out = new Map();
|
|
98
59
|
for (const [id, creds] of credentials) {
|
|
@@ -105,10 +66,6 @@ export function realUpstreamCredentialsForSidecar(credentials, realUpstreamBaseU
|
|
|
105
66
|
}
|
|
106
67
|
return out;
|
|
107
68
|
}
|
|
108
|
-
/**
|
|
109
|
-
* 从缓存文件加载「厂商 -> 真实 baseUrl」映射。文件不存在或解析失败返回空 Map。
|
|
110
|
-
* 供插件 sync 时使用(先读已有缓存,没有则当空)。
|
|
111
|
-
*/
|
|
112
69
|
export async function loadRealUpstreamBaseUrlCache() {
|
|
113
70
|
try {
|
|
114
71
|
const raw = await fs.promises.readFile(REAL_UPSTREAM_BASEURL_CACHE_FILE, "utf8");
|
|
@@ -126,24 +83,28 @@ export async function loadRealUpstreamBaseUrlCache() {
|
|
|
126
83
|
return new Map();
|
|
127
84
|
}
|
|
128
85
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const map = new Map();
|
|
137
|
-
if (obj && typeof obj === "object") {
|
|
138
|
-
for (const [id, url] of Object.entries(obj)) {
|
|
139
|
-
if (id && typeof url === "string" && url.trim())
|
|
140
|
-
map.set(id, url.trim());
|
|
86
|
+
export async function saveRealUpstreamBaseUrlCache(map) {
|
|
87
|
+
try {
|
|
88
|
+
await fs.promises.mkdir(PLUGIN_DIR, { recursive: true });
|
|
89
|
+
const obj = {};
|
|
90
|
+
for (const [id, url] of map) {
|
|
91
|
+
if (id !== "memoryx-gateway" && url)
|
|
92
|
+
obj[id] = url;
|
|
141
93
|
}
|
|
94
|
+
await fs.promises.writeFile(REAL_UPSTREAM_BASEURL_CACHE_FILE, JSON.stringify(obj, null, 2), "utf8");
|
|
95
|
+
log(`[Proxy] baseUrl cache saved`);
|
|
96
|
+
}
|
|
97
|
+
catch (e) {
|
|
98
|
+
log(`[Proxy] Failed to save real upstream cache: ${e?.message ?? e}`);
|
|
142
99
|
}
|
|
143
|
-
|
|
100
|
+
}
|
|
101
|
+
export function isLocalhostBaseUrl(url, sidecarBaseUrl) {
|
|
102
|
+
const u = (url || "").trim().toLowerCase().replace(/\/$/, "");
|
|
103
|
+
const sidecarNorm = (sidecarBaseUrl || "").toLowerCase().replace(/\/$/, "");
|
|
104
|
+
return !u || u.includes("localhost") || (!!sidecarNorm && u === sidecarNorm);
|
|
144
105
|
}
|
|
145
106
|
/**
|
|
146
|
-
*
|
|
107
|
+
* Sync real-upstream-baseurl.json: config baseUrl vs cache, update non-localhost entries.
|
|
147
108
|
*/
|
|
148
109
|
export async function syncRealUpstreamBaseUrlCacheFromConfig(config, sidecarBaseUrl) {
|
|
149
110
|
let cached;
|
|
@@ -165,40 +126,12 @@ export async function syncRealUpstreamBaseUrlCacheFromConfig(config, sidecarBase
|
|
|
165
126
|
}
|
|
166
127
|
await saveRealUpstreamBaseUrlCache(merged);
|
|
167
128
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
export async function saveRealUpstreamBaseUrlCache(map) {
|
|
172
|
-
try {
|
|
173
|
-
await fs.promises.mkdir(PLUGIN_DIR, { recursive: true });
|
|
174
|
-
const obj = {};
|
|
175
|
-
for (const [id, url] of map) {
|
|
176
|
-
if (id !== "memoryx-gateway" && url)
|
|
177
|
-
obj[id] = url;
|
|
178
|
-
}
|
|
179
|
-
await fs.promises.writeFile(REAL_UPSTREAM_BASEURL_CACHE_FILE, JSON.stringify(obj, null, 2), "utf8");
|
|
180
|
-
log(`[Proxy] Saved real upstream baseUrl cache to ${REAL_UPSTREAM_BASEURL_CACHE_FILE}`);
|
|
181
|
-
}
|
|
182
|
-
catch (e) {
|
|
183
|
-
log(`[Proxy] Failed to save real upstream cache: ${e?.message ?? e}`);
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
/** 判断是否为 localhost(或等于 sidecarBaseUrl),用于合并缓存时决定是否用缓存值覆盖。 */
|
|
187
|
-
export function isLocalhostBaseUrl(url, sidecarBaseUrl) {
|
|
188
|
-
const u = (url || "").trim().toLowerCase().replace(/\/$/, "");
|
|
189
|
-
const sidecarNorm = (sidecarBaseUrl || "").toLowerCase().replace(/\/$/, "");
|
|
190
|
-
return !u || u.includes("localhost") || (!!sidecarNorm && u === sidecarNorm);
|
|
191
|
-
}
|
|
192
|
-
/** Only include providers with non-empty apiKey to avoid invalid Bearer header */
|
|
193
|
-
function hasApiKey(creds) {
|
|
194
|
-
return ((creds.apiKey ?? "").trim().length > 0);
|
|
195
|
-
}
|
|
196
|
-
/** Get all available provider/model list (for server fallback/retry); only providers with apiKey set */
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
// Provider list helpers (pure memory)
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
197
132
|
export function getAvailableProviders(credentials) {
|
|
198
133
|
const result = [];
|
|
199
134
|
for (const [providerId, creds] of credentials) {
|
|
200
|
-
if (!hasApiKey(creds))
|
|
201
|
-
continue;
|
|
202
135
|
if (creds.models && creds.models.length > 0) {
|
|
203
136
|
for (const m of creds.models) {
|
|
204
137
|
const id = m.id || m.name;
|
|
@@ -206,21 +139,15 @@ export function getAvailableProviders(credentials) {
|
|
|
206
139
|
result.push({ provider: providerId, model: id });
|
|
207
140
|
}
|
|
208
141
|
}
|
|
209
|
-
else {
|
|
142
|
+
else if (creds.baseUrl) {
|
|
210
143
|
result.push({ provider: providerId, model: "" });
|
|
211
144
|
}
|
|
212
145
|
}
|
|
213
146
|
return result;
|
|
214
147
|
}
|
|
215
|
-
/**
|
|
216
|
-
* Default primary: one model per provider (first listed); only providers with apiKey set.
|
|
217
|
-
* Order follows config.models.providers key order (Map insertion order).
|
|
218
|
-
*/
|
|
219
148
|
export function getDefaultProviderModelList(credentials) {
|
|
220
149
|
const result = [];
|
|
221
150
|
for (const [providerId, creds] of credentials) {
|
|
222
|
-
if (!hasApiKey(creds))
|
|
223
|
-
continue;
|
|
224
151
|
if (creds.models?.length) {
|
|
225
152
|
const id = creds.models[0].id || creds.models[0].name;
|
|
226
153
|
if (id)
|
|
@@ -229,7 +156,6 @@ export function getDefaultProviderModelList(credentials) {
|
|
|
229
156
|
}
|
|
230
157
|
return result;
|
|
231
158
|
}
|
|
232
|
-
/** Find provider/model in available list matching primary (primary format: "provider/modelId") */
|
|
233
159
|
function resolvePrimaryInAvailable(primary, available) {
|
|
234
160
|
if (!primary || typeof primary !== "string")
|
|
235
161
|
return null;
|
|
@@ -247,7 +173,6 @@ function resolvePrimaryInAvailable(primary, available) {
|
|
|
247
173
|
return { provider: wantProvider, model: wantModel };
|
|
248
174
|
return null;
|
|
249
175
|
}
|
|
250
|
-
/** Get default model/provider and full availableProviders; prefer config.agents.defaults.model.primary, else first in list. */
|
|
251
176
|
export function getDefaultModelAndProvider(credentials, config) {
|
|
252
177
|
const defaultList = getDefaultProviderModelList(credentials);
|
|
253
178
|
const availableAll = getAvailableProviders(credentials);
|
|
@@ -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":"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"}
|
package/dist/proxy-redirect.js
CHANGED
|
@@ -1,22 +1,40 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Redirect provider baseUrl to Sidecar and wrap config so reads always resolve to Sidecar.
|
|
3
|
+
*
|
|
4
|
+
* Neither function performs file I/O. They only mutate api.config in memory.
|
|
3
5
|
*/
|
|
4
|
-
import { extractProviderCredentials } from "./proxy-credentials.js";
|
|
5
6
|
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
|
+
}
|
|
6
26
|
export function createProxyRedirect(api, getSidecarBase, realProviderHeader) {
|
|
7
27
|
const applySidecarRedirect = (opts) => {
|
|
8
28
|
const sidecarBase = getSidecarBase();
|
|
9
|
-
const
|
|
10
|
-
if (
|
|
29
|
+
const providerIds = getProviderIds(api.config);
|
|
30
|
+
if (providerIds.length === 0) {
|
|
11
31
|
if (!opts?.quiet)
|
|
12
|
-
log(`[Proxy] No providers to redirect
|
|
32
|
+
log(`[Proxy] No providers to redirect`, { console: true });
|
|
13
33
|
return;
|
|
14
34
|
}
|
|
15
35
|
const providers = api.config.models.providers;
|
|
16
36
|
let n = 0;
|
|
17
|
-
for (const providerId of
|
|
18
|
-
if (providerId === "memoryx-gateway")
|
|
19
|
-
continue;
|
|
37
|
+
for (const providerId of providerIds) {
|
|
20
38
|
const p = providers[providerId];
|
|
21
39
|
if (!p || typeof p !== "object")
|
|
22
40
|
continue;
|
|
@@ -28,31 +46,20 @@ export function createProxyRedirect(api, getSidecarBase, realProviderHeader) {
|
|
|
28
46
|
log(`[Proxy] Redirected provider "${providerId}" baseUrl → Sidecar, header ${realProviderHeader}=${providerId}`);
|
|
29
47
|
}
|
|
30
48
|
if (n > 0 && !opts?.quiet) {
|
|
31
|
-
api.logger.info(`[MemoryX] ✅ Sidecar redirect applied for ${n} provider(s)
|
|
32
|
-
log(`[Proxy] Sidecar redirect applied for ${n} provider(s)`, { console: true });
|
|
33
|
-
}
|
|
34
|
-
if (n > 0) {
|
|
35
|
-
for (const providerId of creds.keys()) {
|
|
36
|
-
if (providerId === "memoryx-gateway")
|
|
37
|
-
continue;
|
|
38
|
-
const p = providers[providerId];
|
|
39
|
-
const readBack = (p && typeof p === "object" && p.baseUrl) ? p.baseUrl : "<missing>";
|
|
40
|
-
const isSidecar = readBack === sidecarBase;
|
|
41
|
-
log(`[Proxy] Readback ${providerId}.baseUrl = ${readBack} ${isSidecar ? "(Sidecar ✓)" : "(NOT Sidecar!)"}`);
|
|
42
|
-
}
|
|
49
|
+
api.logger.info(`[MemoryX] ✅ Sidecar redirect applied for ${n} provider(s).`);
|
|
43
50
|
}
|
|
44
51
|
};
|
|
45
52
|
const wrapProvidersWithProxy = () => {
|
|
46
53
|
if (!api.config?.models?.providers || typeof api.config.models.providers !== "object")
|
|
47
54
|
return;
|
|
48
|
-
const
|
|
49
|
-
if (
|
|
55
|
+
const providerIds = new Set(getProviderIds(api.config));
|
|
56
|
+
if (providerIds.size === 0)
|
|
50
57
|
return;
|
|
51
58
|
const raw = api.config.models.providers;
|
|
52
59
|
const handler = {
|
|
53
60
|
get(target, prop) {
|
|
54
61
|
const val = target[prop];
|
|
55
|
-
if (val && typeof val === "object" &&
|
|
62
|
+
if (val && typeof val === "object" && providerIds.has(prop)) {
|
|
56
63
|
return new Proxy(val, {
|
|
57
64
|
get(t, k) {
|
|
58
65
|
if (k === "baseUrl")
|
package/dist/sidecar.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Local HTTP Sidecar:
|
|
3
|
-
*
|
|
2
|
+
* Local HTTP Sidecar: lightweight forwarder.
|
|
3
|
+
* Reads base_url_map from local cache file, credentials from constructor (api.config).
|
|
4
|
+
* Packages payload and POSTs to MemoryX API server.
|
|
4
5
|
*/
|
|
5
6
|
import type { ProviderCredentials } from "./types.js";
|
|
6
7
|
import type { PluginConfig } from "./types.js";
|
|
@@ -27,7 +28,6 @@ export declare class SidecarServer {
|
|
|
27
28
|
start(): Promise<void>;
|
|
28
29
|
stop(): Promise<void>;
|
|
29
30
|
getPort(): number;
|
|
30
|
-
/** 轻量化:只读 base_url_map + credentials,打包发给服务端;不做 provider 解析、无 target。 */
|
|
31
31
|
private handleRequest;
|
|
32
32
|
private readBody;
|
|
33
33
|
}
|
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;;;;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;IAwBtB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAa3B,OAAO,IAAI,MAAM;YAMH,aAAa;IAgM3B,OAAO,CAAC,QAAQ;CAQnB"}
|
package/dist/sidecar.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Local HTTP Sidecar:
|
|
3
|
-
*
|
|
2
|
+
* Local HTTP Sidecar: lightweight forwarder.
|
|
3
|
+
* Reads base_url_map from local cache file, credentials from constructor (api.config).
|
|
4
|
+
* Packages payload and POSTs to MemoryX API server.
|
|
4
5
|
*/
|
|
5
6
|
import * as http from "http";
|
|
6
7
|
import { SIDECAR_PORT } from "./constants.js";
|
|
@@ -60,7 +61,6 @@ export class SidecarServer {
|
|
|
60
61
|
? this.server.address().port
|
|
61
62
|
: SIDECAR_PORT;
|
|
62
63
|
}
|
|
63
|
-
/** 轻量化:只读 base_url_map + credentials,打包发给服务端;不做 provider 解析、无 target。 */
|
|
64
64
|
async handleRequest(req, res) {
|
|
65
65
|
const url = req.url || "/";
|
|
66
66
|
const method = req.method?.toUpperCase();
|
|
@@ -71,6 +71,7 @@ export class SidecarServer {
|
|
|
71
71
|
return;
|
|
72
72
|
}
|
|
73
73
|
const reqId = `req-${Date.now()}`;
|
|
74
|
+
log(`[Sidecar] ${reqId} in ${method} ${url}`, { console: true });
|
|
74
75
|
let body;
|
|
75
76
|
try {
|
|
76
77
|
body = await this.readBody(req);
|
|
@@ -89,6 +90,7 @@ export class SidecarServer {
|
|
|
89
90
|
openaiRequest = {};
|
|
90
91
|
}
|
|
91
92
|
const stream = !!openaiRequest.stream;
|
|
93
|
+
// Read base_url_map from cache file (async)
|
|
92
94
|
let baseUrlMap;
|
|
93
95
|
try {
|
|
94
96
|
baseUrlMap = await loadRealUpstreamBaseUrlCache();
|
|
@@ -101,16 +103,20 @@ export class SidecarServer {
|
|
|
101
103
|
if (id && u?.trim())
|
|
102
104
|
baseUrlMapObj[id] = u.trim();
|
|
103
105
|
}
|
|
106
|
+
const rawProvider = req.headers["x-memoryx-real-provider"]?.trim() || "";
|
|
107
|
+
const authHeader = req.headers["authorization"]?.trim() || "";
|
|
108
|
+
const incomingApiKey = authHeader.startsWith("Bearer ")
|
|
109
|
+
? authHeader.slice(7).trim()
|
|
110
|
+
: authHeader;
|
|
111
|
+
// Build credentials: use incoming Authorization header as apiKey for the target provider
|
|
104
112
|
const credentialsObj = {};
|
|
105
|
-
for (const [id,
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
};
|
|
113
|
+
for (const [id, creds] of this.credentials) {
|
|
114
|
+
const realBaseUrl = baseUrlMapObj[id] || creds.baseUrl || "";
|
|
115
|
+
const apiKey = (id === rawProvider && incomingApiKey)
|
|
116
|
+
? incomingApiKey
|
|
117
|
+
: (creds.apiKey || "");
|
|
118
|
+
credentialsObj[id] = { baseUrl: realBaseUrl, apiKey };
|
|
112
119
|
}
|
|
113
|
-
const rawProvider = req.headers["x-memoryx-real-provider"]?.trim() || "";
|
|
114
120
|
const messages = openaiRequest.messages || [];
|
|
115
121
|
const lastUserMsg = [...messages].reverse().find((m) => m.role === "user");
|
|
116
122
|
const searchQuery = typeof lastUserMsg?.content === "string" ? lastUserMsg.content : "";
|
|
@@ -128,6 +134,7 @@ export class SidecarServer {
|
|
|
128
134
|
const proxyRequestBody = {
|
|
129
135
|
base_url_map: baseUrlMapObj,
|
|
130
136
|
credentials: credentialsObj,
|
|
137
|
+
incoming_api_key: incomingApiKey || undefined,
|
|
131
138
|
body: openaiRequest,
|
|
132
139
|
request_headers: { "x-memoryx-real-provider": rawProvider },
|
|
133
140
|
search_query: searchQuery,
|
|
@@ -135,18 +142,25 @@ export class SidecarServer {
|
|
|
135
142
|
available_models: this.availableProviders.map((p) => `${p.provider}/${p.model}`),
|
|
136
143
|
};
|
|
137
144
|
const pathLog = (req.url || "/").split("?")[0] || "/";
|
|
138
|
-
log(`[Sidecar] ${reqId} ${method} ${pathLog} model=${openaiRequest.model ?? "-"} stream=${stream} → proxy
|
|
145
|
+
log(`[Sidecar] ${reqId} ${method} ${pathLog} model=${openaiRequest.model ?? "-"} stream=${stream} → proxy ${proxyUrl}`, { console: true });
|
|
146
|
+
const fetchAbort = new AbortController();
|
|
147
|
+
const fetchTimer = setTimeout(() => fetchAbort.abort(), 120_000);
|
|
139
148
|
let proxyResponse = null;
|
|
140
149
|
try {
|
|
141
150
|
proxyResponse = await fetch(proxyUrl, {
|
|
142
151
|
method: "POST",
|
|
143
152
|
headers: { "Content-Type": "application/json", "X-API-Key": memoryxApiKey },
|
|
144
153
|
body: JSON.stringify(proxyRequestBody),
|
|
154
|
+
signal: fetchAbort.signal,
|
|
145
155
|
});
|
|
146
156
|
}
|
|
147
157
|
catch (e) {
|
|
148
158
|
proxyResponse = null;
|
|
149
|
-
|
|
159
|
+
const msg = fetchAbort.signal.aborted ? "fetch timeout (120s)" : (e?.message ?? String(e));
|
|
160
|
+
log(`[Sidecar] ${reqId} err proxy fetch: ${msg}`, { console: true });
|
|
161
|
+
}
|
|
162
|
+
finally {
|
|
163
|
+
clearTimeout(fetchTimer);
|
|
150
164
|
}
|
|
151
165
|
const proxyFailed = !proxyResponse || proxyResponse.status >= 500;
|
|
152
166
|
if (proxyFailed) {
|
|
@@ -173,15 +187,20 @@ export class SidecarServer {
|
|
|
173
187
|
const proxy = proxyResponse;
|
|
174
188
|
if (openaiRequest.stream && proxy.body) {
|
|
175
189
|
const reader = proxy.body.getReader();
|
|
190
|
+
const CHUNK_TIMEOUT = 90_000;
|
|
191
|
+
const readWithTimeout = () => Promise.race([
|
|
192
|
+
reader.read(),
|
|
193
|
+
new Promise((_, rej) => setTimeout(() => rej(new Error("chunk timeout")), CHUNK_TIMEOUT)),
|
|
194
|
+
]);
|
|
176
195
|
let firstChunk;
|
|
177
196
|
try {
|
|
178
|
-
firstChunk = await
|
|
179
|
-
reader.read(),
|
|
180
|
-
new Promise((_, rej) => setTimeout(() => rej(new Error("First chunk timeout")), 15000)),
|
|
181
|
-
]);
|
|
197
|
+
firstChunk = await readWithTimeout();
|
|
182
198
|
}
|
|
183
199
|
catch (firstErr) {
|
|
184
|
-
|
|
200
|
+
try {
|
|
201
|
+
reader.releaseLock();
|
|
202
|
+
}
|
|
203
|
+
catch (_) { }
|
|
185
204
|
log(`[Sidecar] ${reqId} err stream first chunk: ${firstErr?.message ?? "timeout"}`, { console: true });
|
|
186
205
|
res.writeHead(502, { "Content-Type": "application/json" });
|
|
187
206
|
res.end(JSON.stringify({ error: "MemoryX proxy stream failed" }));
|
|
@@ -194,17 +213,27 @@ export class SidecarServer {
|
|
|
194
213
|
if (!firstChunk.done && firstChunk.value)
|
|
195
214
|
res.write(Buffer.from(firstChunk.value));
|
|
196
215
|
while (true) {
|
|
197
|
-
|
|
198
|
-
|
|
216
|
+
if (res.destroyed) {
|
|
217
|
+
log(`[Sidecar] ${reqId} client disconnected, aborting stream`, { console: true });
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
const { done, value } = await readWithTimeout();
|
|
221
|
+
if (done || !value)
|
|
199
222
|
break;
|
|
200
223
|
res.write(Buffer.from(value));
|
|
201
224
|
}
|
|
202
225
|
}
|
|
226
|
+
catch (streamErr) {
|
|
227
|
+
log(`[Sidecar] ${reqId} stream error: ${streamErr?.message ?? "unknown"}`, { console: true });
|
|
228
|
+
}
|
|
203
229
|
finally {
|
|
204
|
-
|
|
230
|
+
try {
|
|
231
|
+
reader.releaseLock();
|
|
232
|
+
}
|
|
233
|
+
catch (_) { }
|
|
205
234
|
}
|
|
206
235
|
res.end();
|
|
207
|
-
log(`[Sidecar] ${reqId} ← ${proxy.status} stream
|
|
236
|
+
log(`[Sidecar] ${reqId} ← ${proxy.status} stream`, { console: true });
|
|
208
237
|
return;
|
|
209
238
|
}
|
|
210
239
|
if (!proxy.body) {
|
|
@@ -212,11 +241,11 @@ export class SidecarServer {
|
|
|
212
241
|
"Content-Type": proxy.headers.get("content-type") || "application/json",
|
|
213
242
|
});
|
|
214
243
|
res.end();
|
|
215
|
-
log(`[Sidecar] ${reqId} ← ${proxy.status} (no body)
|
|
244
|
+
log(`[Sidecar] ${reqId} ← ${proxy.status} (no body)`, { console: true });
|
|
216
245
|
return;
|
|
217
246
|
}
|
|
218
247
|
const text = await proxy.text();
|
|
219
|
-
log(`[Sidecar] ${reqId} ← ${proxy.status} body=${text.length}
|
|
248
|
+
log(`[Sidecar] ${reqId} ← ${proxy.status} body=${text.length}`, { console: true });
|
|
220
249
|
res.writeHead(proxy.status, {
|
|
221
250
|
"Content-Type": proxy.headers.get("content-type") || "application/json",
|
|
222
251
|
});
|
package/package.json
CHANGED