@evomap/evolver-proxy 2.0.0-beta.18 → 2.0.0-beta.19
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/bin/evolver-proxy.d.ts +11 -0
- package/dist/bin/evolver-proxy.js +54 -10
- package/dist/daemon/collaborationFacade.js +23 -13
- package/dist/daemon/proxyDaemon.d.ts +50 -0
- package/dist/daemon/proxyDaemon.js +1026 -24
- package/dist/daemon/publishRecallVerifier.d.ts +114 -0
- package/dist/daemon/publishRecallVerifier.js +495 -0
- package/dist/daemon/systemdNotifier.d.ts +46 -0
- package/dist/daemon/systemdNotifier.js +153 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/lifecycle/claimNudge.d.ts +20 -0
- package/dist/lifecycle/claimNudge.js +124 -0
- package/dist/lifecycle/manager.d.ts +4 -0
- package/dist/lifecycle/manager.js +15 -2
- package/dist/llm/upstream.d.ts +5 -1
- package/dist/llm/upstream.js +24 -1
- package/dist/sync/engine.d.ts +12 -0
- package/dist/sync/engine.js +255 -64
- package/package.json +6 -3
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
const SYSTEMD_NOTIFY_TIMEOUT_MS = 5_000;
|
|
3
|
+
const MIN_WATCHDOG_INTERVAL_MS = 1_000;
|
|
4
|
+
const DEFAULT_READY_RETRY_DELAYS_MS = [250, 750];
|
|
5
|
+
const MAX_READY_RETRIES = 4;
|
|
6
|
+
const MAX_READY_RETRY_DELAY_MS = 5_000;
|
|
7
|
+
const defaultSystemdNotifyExec = (command, args, options, callback) => {
|
|
8
|
+
execFile(command, [...args], options, (error) => { callback(error); });
|
|
9
|
+
};
|
|
10
|
+
export function systemdWatchdogIntervalMs(env = process.env) {
|
|
11
|
+
const usec = parsePositiveSafeInteger(env['WATCHDOG_USEC']);
|
|
12
|
+
if (usec === undefined)
|
|
13
|
+
return 0;
|
|
14
|
+
return Math.max(MIN_WATCHDOG_INTERVAL_MS, Math.floor(usec / 2_000));
|
|
15
|
+
}
|
|
16
|
+
export class SystemdNotifier {
|
|
17
|
+
options;
|
|
18
|
+
env;
|
|
19
|
+
platform;
|
|
20
|
+
now;
|
|
21
|
+
execFile;
|
|
22
|
+
readyRetryDelaysMs;
|
|
23
|
+
sleep;
|
|
24
|
+
timer;
|
|
25
|
+
readySent = false;
|
|
26
|
+
readyInFlight;
|
|
27
|
+
constructor(options) {
|
|
28
|
+
this.options = options;
|
|
29
|
+
this.env = options.env ?? process.env;
|
|
30
|
+
this.platform = options.platform ?? process.platform;
|
|
31
|
+
this.now = options.now ?? Date.now;
|
|
32
|
+
this.execFile = options.execFile ?? defaultSystemdNotifyExec;
|
|
33
|
+
this.readyRetryDelaysMs = normalizeReadyRetryDelays(options.readyRetryDelaysMs ?? DEFAULT_READY_RETRY_DELAYS_MS);
|
|
34
|
+
this.sleep = options.sleep ?? sleepMs;
|
|
35
|
+
}
|
|
36
|
+
async ready() {
|
|
37
|
+
if (!this.active())
|
|
38
|
+
return false;
|
|
39
|
+
if (this.readySent)
|
|
40
|
+
return true;
|
|
41
|
+
if (this.readyInFlight)
|
|
42
|
+
return this.readyInFlight;
|
|
43
|
+
const health = this.readHealth();
|
|
44
|
+
if (!health?.running || !health.ipcListening || !health.lifecycleArmed)
|
|
45
|
+
return false;
|
|
46
|
+
const attempt = this.announceReady();
|
|
47
|
+
this.readyInFlight = attempt;
|
|
48
|
+
void attempt.then(() => { if (this.readyInFlight === attempt)
|
|
49
|
+
this.readyInFlight = undefined; }, () => { if (this.readyInFlight === attempt)
|
|
50
|
+
this.readyInFlight = undefined; });
|
|
51
|
+
return attempt;
|
|
52
|
+
}
|
|
53
|
+
async readyOrThrow() {
|
|
54
|
+
if (!this.active())
|
|
55
|
+
return;
|
|
56
|
+
if (!await this.ready())
|
|
57
|
+
throw new Error('systemd_ready_notification_failed');
|
|
58
|
+
}
|
|
59
|
+
stop() {
|
|
60
|
+
if (this.timer) {
|
|
61
|
+
clearInterval(this.timer);
|
|
62
|
+
this.timer = undefined;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
active() {
|
|
66
|
+
return this.platform === 'linux' && Boolean(this.env['NOTIFY_SOCKET']?.trim());
|
|
67
|
+
}
|
|
68
|
+
startWatchdog() {
|
|
69
|
+
if (this.timer)
|
|
70
|
+
return;
|
|
71
|
+
// The installed unit may run a stable recovery controller as MainPID and the
|
|
72
|
+
// proxy as its child. NotifyAccess=all intentionally authorizes that child.
|
|
73
|
+
const intervalMs = systemdWatchdogIntervalMs(this.env);
|
|
74
|
+
if (intervalMs === 0)
|
|
75
|
+
return;
|
|
76
|
+
this.timer = setInterval(() => { this.pingWatchdog(intervalMs); }, intervalMs);
|
|
77
|
+
this.timer.unref?.();
|
|
78
|
+
}
|
|
79
|
+
pingWatchdog(freshnessMs) {
|
|
80
|
+
const health = this.readHealth();
|
|
81
|
+
if (!health?.running || !health.ipcListening || !health.lifecycleArmed)
|
|
82
|
+
return;
|
|
83
|
+
if (health.consecutiveFailures !== 0 || health.lastTickAt === undefined)
|
|
84
|
+
return;
|
|
85
|
+
const ageMs = this.now() - health.lastTickAt;
|
|
86
|
+
const plannedSleepHealthy = health.nextTickDueAt !== undefined
|
|
87
|
+
&& this.now() <= health.nextTickDueAt + freshnessMs;
|
|
88
|
+
if (!Number.isFinite(ageMs) || ageMs < 0 || (ageMs > freshnessMs && !plannedSleepHealthy))
|
|
89
|
+
return;
|
|
90
|
+
void this.notify('WATCHDOG=1');
|
|
91
|
+
}
|
|
92
|
+
async announceReady() {
|
|
93
|
+
const attempts = this.readyRetryDelaysMs.length + 1;
|
|
94
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
95
|
+
const health = this.readHealth();
|
|
96
|
+
if (!health?.running || !health.ipcListening || !health.lifecycleArmed)
|
|
97
|
+
return false;
|
|
98
|
+
if (await this.notify('READY=1')) {
|
|
99
|
+
this.readySent = true;
|
|
100
|
+
this.startWatchdog();
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
const delayMs = this.readyRetryDelaysMs[attempt];
|
|
104
|
+
if (delayMs !== undefined) {
|
|
105
|
+
try {
|
|
106
|
+
await this.sleep(delayMs);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
readHealth() {
|
|
116
|
+
try {
|
|
117
|
+
return this.options.health();
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
notify(state) {
|
|
124
|
+
return new Promise((resolve) => {
|
|
125
|
+
try {
|
|
126
|
+
this.execFile('systemd-notify', [state], {
|
|
127
|
+
env: this.env,
|
|
128
|
+
timeout: SYSTEMD_NOTIFY_TIMEOUT_MS,
|
|
129
|
+
windowsHide: true,
|
|
130
|
+
}, (error) => { resolve(error === null); });
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
// Keep delivery failures as data: READY is enforced by readyOrThrow(), while watchdog pings stay best-effort.
|
|
134
|
+
resolve(false);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function normalizeReadyRetryDelays(values) {
|
|
140
|
+
return values.slice(0, MAX_READY_RETRIES).map((value) => (Number.isFinite(value)
|
|
141
|
+
? Math.min(MAX_READY_RETRY_DELAY_MS, Math.max(0, Math.floor(value)))
|
|
142
|
+
: 0));
|
|
143
|
+
}
|
|
144
|
+
function sleepMs(delayMs) {
|
|
145
|
+
return new Promise((resolve) => { setTimeout(resolve, delayMs); });
|
|
146
|
+
}
|
|
147
|
+
function parsePositiveSafeInteger(value) {
|
|
148
|
+
const trimmed = value?.trim();
|
|
149
|
+
if (!trimmed || !/^\d+$/.test(trimmed))
|
|
150
|
+
return undefined;
|
|
151
|
+
const parsed = Number(trimmed);
|
|
152
|
+
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : undefined;
|
|
153
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
export declare const PACKAGE = "@evomap/evolver-proxy";
|
|
2
2
|
export * from './sync/engine.js';
|
|
3
3
|
export * from './lifecycle/manager.js';
|
|
4
|
+
export * from './lifecycle/claimNudge.js';
|
|
4
5
|
export * from './daemon/proxyDaemon.js';
|
|
6
|
+
export * from './daemon/publishRecallVerifier.js';
|
|
5
7
|
export * from './lifecycle/deployGuard.js';
|
|
6
8
|
export * from './router/index.js';
|
|
7
9
|
export * from './llm/index.js';
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
export const PACKAGE = '@evomap/evolver-proxy';
|
|
2
2
|
export * from './sync/engine.js';
|
|
3
3
|
export * from './lifecycle/manager.js';
|
|
4
|
+
export * from './lifecycle/claimNudge.js';
|
|
4
5
|
export * from './daemon/proxyDaemon.js';
|
|
6
|
+
export * from './daemon/publishRecallVerifier.js';
|
|
5
7
|
export * from './lifecycle/deployGuard.js';
|
|
6
8
|
export * from './router/index.js';
|
|
7
9
|
export * from './llm/index.js';
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface ClaimNudgeHelloResult {
|
|
2
|
+
ok: boolean;
|
|
3
|
+
claimCode?: string;
|
|
4
|
+
claimUrl?: string;
|
|
5
|
+
}
|
|
6
|
+
export interface ClaimNudgeStateStore {
|
|
7
|
+
getState(key: string): string | undefined;
|
|
8
|
+
setState(key: string, value: string): void;
|
|
9
|
+
}
|
|
10
|
+
export interface ClaimNudgeOptions {
|
|
11
|
+
store: ClaimNudgeStateStore;
|
|
12
|
+
hubUrl: string;
|
|
13
|
+
env?: Readonly<Record<string, string | undefined>>;
|
|
14
|
+
now?: () => number;
|
|
15
|
+
write?: (text: string) => void;
|
|
16
|
+
}
|
|
17
|
+
export type ClaimNudge = (result: ClaimNudgeHelloResult) => boolean;
|
|
18
|
+
export declare function createClaimNudge(options: ClaimNudgeOptions): ClaimNudge;
|
|
19
|
+
export declare function wrapHelloWithClaimNudge<T extends ClaimNudgeHelloResult, O>(hello: (options: O) => Promise<T>, nudge: ClaimNudge): (options: O) => Promise<T>;
|
|
20
|
+
export declare function claimNudgeCooldownMs(raw: string | undefined): number;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
const DEFAULT_COOLDOWN_MS = 6 * 60 * 60_000;
|
|
2
|
+
const MIN_COOLDOWN_MS = 60_000;
|
|
3
|
+
const MAX_COOLDOWN_MS = 30 * 24 * 60 * 60_000;
|
|
4
|
+
const MAX_STATE_ENTRIES = 32;
|
|
5
|
+
const STATE_KEY = 'lifecycle:claim_nudge:v1';
|
|
6
|
+
const CLAIM_CODE_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{1,127}$/;
|
|
7
|
+
export function createClaimNudge(options) {
|
|
8
|
+
const env = options.env ?? process.env;
|
|
9
|
+
const now = options.now ?? (() => Date.now());
|
|
10
|
+
const write = options.write ?? ((text) => { process.stderr.write(text); });
|
|
11
|
+
let memory = { version: 1, entries: {} };
|
|
12
|
+
return (result) => {
|
|
13
|
+
if (!result.ok || env['EVOLVER_DISABLE_CLAIM_NUDGE'] === '1')
|
|
14
|
+
return false;
|
|
15
|
+
const code = normalizeClaimCode(result.claimCode);
|
|
16
|
+
const url = code ? trustedClaimUrl(result.claimUrl, options.hubUrl) : undefined;
|
|
17
|
+
if (!code || !url)
|
|
18
|
+
return false;
|
|
19
|
+
const at = now();
|
|
20
|
+
const cooldownMs = claimNudgeCooldownMs(env['EVOLVER_CLAIM_NUDGE_COOLDOWN_MS']);
|
|
21
|
+
const state = mergeState(readState(options.store), memory);
|
|
22
|
+
const lastPrintedAt = state.entries[code] ?? 0;
|
|
23
|
+
if (lastPrintedAt > 0 && at - lastPrintedAt < cooldownMs)
|
|
24
|
+
return false;
|
|
25
|
+
const message = [
|
|
26
|
+
'',
|
|
27
|
+
'[evolver-proxy] This node is not linked to an EvoMap web account.',
|
|
28
|
+
`Claim URL: ${url}`,
|
|
29
|
+
`Claim code: ${code}`,
|
|
30
|
+
'Claiming is optional; the proxy continues to run without it.',
|
|
31
|
+
'',
|
|
32
|
+
].join('\n');
|
|
33
|
+
try {
|
|
34
|
+
write(message);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
memory = pruneState({ ...state.entries, [code]: at });
|
|
40
|
+
try {
|
|
41
|
+
options.store.setState(STATE_KEY, JSON.stringify(memory));
|
|
42
|
+
}
|
|
43
|
+
catch { /* memory still suppresses repeats */ }
|
|
44
|
+
return true;
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
export function wrapHelloWithClaimNudge(hello, nudge) {
|
|
48
|
+
return async (options) => {
|
|
49
|
+
const result = await hello(options);
|
|
50
|
+
try {
|
|
51
|
+
nudge(result);
|
|
52
|
+
}
|
|
53
|
+
catch { /* a terminal nudge must never break hello */ }
|
|
54
|
+
return result;
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
export function claimNudgeCooldownMs(raw) {
|
|
58
|
+
const parsed = Number(raw);
|
|
59
|
+
if (!Number.isFinite(parsed) || parsed <= 0)
|
|
60
|
+
return DEFAULT_COOLDOWN_MS;
|
|
61
|
+
return Math.max(MIN_COOLDOWN_MS, Math.min(Math.floor(parsed), MAX_COOLDOWN_MS));
|
|
62
|
+
}
|
|
63
|
+
function normalizeClaimCode(value) {
|
|
64
|
+
const code = value?.trim();
|
|
65
|
+
return code && CLAIM_CODE_RE.test(code) ? code : undefined;
|
|
66
|
+
}
|
|
67
|
+
function trustedClaimUrl(value, hubUrl) {
|
|
68
|
+
const raw = value?.trim();
|
|
69
|
+
if (!raw || raw.length > 2_048)
|
|
70
|
+
return undefined;
|
|
71
|
+
try {
|
|
72
|
+
const url = new URL(raw);
|
|
73
|
+
const hub = new URL(hubUrl);
|
|
74
|
+
if (url.username || url.password)
|
|
75
|
+
return undefined;
|
|
76
|
+
const sameOrigin = url.origin === hub.origin;
|
|
77
|
+
const evomapHost = url.hostname === 'evomap.ai' || url.hostname.endsWith('.evomap.ai');
|
|
78
|
+
if (url.protocol === 'https:' && (sameOrigin || evomapHost))
|
|
79
|
+
return url.toString();
|
|
80
|
+
if (url.protocol === 'http:' && sameOrigin && isLoopback(url.hostname))
|
|
81
|
+
return url.toString();
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
// Invalid or non-absolute URLs are never printed.
|
|
85
|
+
}
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
function isLoopback(hostname) {
|
|
89
|
+
return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '[::1]';
|
|
90
|
+
}
|
|
91
|
+
function readState(store) {
|
|
92
|
+
try {
|
|
93
|
+
const raw = store.getState(STATE_KEY);
|
|
94
|
+
if (!raw)
|
|
95
|
+
return { version: 1, entries: {} };
|
|
96
|
+
const parsed = JSON.parse(raw);
|
|
97
|
+
if (parsed.version !== 1 || !parsed.entries || typeof parsed.entries !== 'object' || Array.isArray(parsed.entries)) {
|
|
98
|
+
return { version: 1, entries: {} };
|
|
99
|
+
}
|
|
100
|
+
const entries = {};
|
|
101
|
+
for (const [code, value] of Object.entries(parsed.entries)) {
|
|
102
|
+
if (CLAIM_CODE_RE.test(code) && typeof value === 'number' && Number.isFinite(value) && value > 0)
|
|
103
|
+
entries[code] = value;
|
|
104
|
+
}
|
|
105
|
+
return { version: 1, entries };
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return { version: 1, entries: {} };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function mergeState(a, b) {
|
|
112
|
+
const entries = { ...a.entries };
|
|
113
|
+
for (const [code, at] of Object.entries(b.entries))
|
|
114
|
+
entries[code] = Math.max(entries[code] ?? 0, at);
|
|
115
|
+
return { version: 1, entries };
|
|
116
|
+
}
|
|
117
|
+
function pruneState(entries) {
|
|
118
|
+
return {
|
|
119
|
+
version: 1,
|
|
120
|
+
entries: Object.fromEntries(Object.entries(entries)
|
|
121
|
+
.sort((left, right) => right[1] - left[1])
|
|
122
|
+
.slice(0, MAX_STATE_ENTRIES)),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
@@ -18,6 +18,8 @@ export interface HelloResult {
|
|
|
18
18
|
ok: boolean;
|
|
19
19
|
authError?: boolean;
|
|
20
20
|
nodeId?: string;
|
|
21
|
+
claimCode?: string;
|
|
22
|
+
claimUrl?: string;
|
|
21
23
|
rateLimitUntilMs?: number;
|
|
22
24
|
error?: string;
|
|
23
25
|
details?: unknown;
|
|
@@ -40,6 +42,7 @@ export interface HeartbeatResult {
|
|
|
40
42
|
httpStatus?: number;
|
|
41
43
|
lastUpdateAck?: LastUpdateAck;
|
|
42
44
|
forceUpdate?: ForceUpdateDirective;
|
|
45
|
+
capabilityGaps?: readonly string[];
|
|
43
46
|
}
|
|
44
47
|
export interface HeartbeatTickResult {
|
|
45
48
|
ok: boolean;
|
|
@@ -98,6 +101,7 @@ export declare class LifecycleManager {
|
|
|
98
101
|
private recordHubUnreachable;
|
|
99
102
|
private clearLegacyNodeSecretVersion;
|
|
100
103
|
private verifyReauthHeartbeat;
|
|
104
|
+
private persistCapabilityGaps;
|
|
101
105
|
private heartbeatOptions;
|
|
102
106
|
private callHello;
|
|
103
107
|
private handleLastUpdateAck;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mailbox, hub as hubNs } from '@evomap/evolver-core';
|
|
1
|
+
import { mailbox, hub as hubNs, signals } from '@evomap/evolver-core';
|
|
2
2
|
import { clearLastUpdateOnAck, isLastUpdateRelatedError, readPendingLastUpdate, shouldClearForLastUpdateAck, } from '../selfUpdate/lastUpdate.js';
|
|
3
3
|
export const DEFAULT_HEARTBEAT_INTERVAL_MS = 360_000;
|
|
4
4
|
export const MIN_HEARTBEAT_INTERVAL_MS = 30_000;
|
|
@@ -125,6 +125,7 @@ export class LifecycleManager {
|
|
|
125
125
|
this.handleLastUpdateAck(res, sentLastUpdate, now);
|
|
126
126
|
this.maybeTriggerForceUpdate(res);
|
|
127
127
|
if (res.ok) {
|
|
128
|
+
this.persistCapabilityGaps(res.capabilityGaps, now);
|
|
128
129
|
this.deps.store.setState(K.hubUnreachableUntil, '0');
|
|
129
130
|
this.deps.store.setState(K.authStatus, 'ok');
|
|
130
131
|
this.deps.store.setState(K.lastError, '');
|
|
@@ -293,8 +294,10 @@ export class LifecycleManager {
|
|
|
293
294
|
const hb = await this.deps.heartbeat(opts);
|
|
294
295
|
this.handleLastUpdateAck(hb, opts?.lastUpdate, now);
|
|
295
296
|
this.maybeTriggerForceUpdate(hb);
|
|
296
|
-
if (hb.ok)
|
|
297
|
+
if (hb.ok) {
|
|
298
|
+
this.persistCapabilityGaps(hb.capabilityGaps, now);
|
|
297
299
|
return 'ok';
|
|
300
|
+
}
|
|
298
301
|
if (isHubUnreachableResult(hb)) {
|
|
299
302
|
this.recordHubUnreachable(hb, now);
|
|
300
303
|
return 'hub_unreachable';
|
|
@@ -309,6 +312,16 @@ export class LifecycleManager {
|
|
|
309
312
|
throw err;
|
|
310
313
|
}
|
|
311
314
|
}
|
|
315
|
+
persistCapabilityGaps(capabilityGaps, observedAt) {
|
|
316
|
+
if (capabilityGaps === undefined)
|
|
317
|
+
return;
|
|
318
|
+
try {
|
|
319
|
+
this.deps.store.setState(signals.CAPABILITY_GAPS_STATE_KEY, signals.serializeCapabilityGapsState(capabilityGaps, observedAt));
|
|
320
|
+
}
|
|
321
|
+
catch {
|
|
322
|
+
// Curriculum is advisory; a failed optional KV write must not turn a healthy heartbeat into a failure.
|
|
323
|
+
}
|
|
324
|
+
}
|
|
312
325
|
heartbeatOptions() {
|
|
313
326
|
const opts = {};
|
|
314
327
|
if (this.deps.evolverVersion)
|
package/dist/llm/upstream.d.ts
CHANGED
|
@@ -49,6 +49,9 @@ export interface BedrockRuntimeFactory {
|
|
|
49
49
|
createInvokeModelCommand(input: BedrockInvokeInput): unknown;
|
|
50
50
|
createInvokeModelWithResponseStreamCommand(input: BedrockInvokeInput): unknown;
|
|
51
51
|
}
|
|
52
|
+
declare function warnDeprecatedOpenAICompatible(env: NodeJS.ProcessEnv): void;
|
|
53
|
+
/** Test helper: reset once-warn latch (unit tests only). */
|
|
54
|
+
declare function resetDeprecatedOpenAICompatibleWarning(): void;
|
|
52
55
|
export declare function resolveOpenAIUpstreamUrl(env?: NodeJS.ProcessEnv): string;
|
|
53
56
|
/** Resolve the upstream base URL. OpenAI-compatible routes never inherit the Anthropic-wide override. */
|
|
54
57
|
export declare function resolveUpstreamUrl(env?: NodeJS.ProcessEnv, upstreamMode?: string): string;
|
|
@@ -65,4 +68,5 @@ export declare function makeAnthropicUpstream(opts?: AnthropicUpstreamOptions):
|
|
|
65
68
|
export declare function makeOpenAIUpstream(opts?: ProviderUpstreamOptions): AnthropicProxy;
|
|
66
69
|
export declare function makeGeminiUpstream(opts?: ProviderUpstreamOptions): AnthropicProxy;
|
|
67
70
|
export declare function makeOllamaUpstream(opts?: ProviderUpstreamOptions): AnthropicProxy;
|
|
68
|
-
export declare function makeVertexUpstream(opts?: ProviderUpstreamOptions): AnthropicProxy;
|
|
71
|
+
export declare function makeVertexUpstream(opts?: ProviderUpstreamOptions): AnthropicProxy;
|
|
72
|
+
export { warnDeprecatedOpenAICompatible, resetDeprecatedOpenAICompatibleWarning };
|
package/dist/llm/upstream.js
CHANGED
|
@@ -32,6 +32,27 @@ async function loadDefaultBedrockRuntime() {
|
|
|
32
32
|
function isOpenAiMode(upstreamMode) {
|
|
33
33
|
return upstreamMode === 'openai';
|
|
34
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* Detect deprecated OPENAI_COMPATIBLE_BASE_URLS env var (#671).
|
|
37
|
+
* V1 multi-base lists are ignored for routing; V2 OpenAI base must pass *.api.openai.com/v1 allowlist
|
|
38
|
+
* via EVOLVER_LLM_OPENAI_BASE_URL / EVOMAP_OPENAI_BASE_URL / OPENAI_BASE_URL.
|
|
39
|
+
* Warn once per process to avoid per-request spam.
|
|
40
|
+
*/
|
|
41
|
+
let warnedDeprecatedOpenAICompatible = false;
|
|
42
|
+
function warnDeprecatedOpenAICompatible(env) {
|
|
43
|
+
const deprecatedValue = env['EVOMAP_OPENAI_COMPATIBLE_BASE_URLS'] || env['EVOLVER_OPENAI_COMPATIBLE_BASE_URLS'];
|
|
44
|
+
if (!deprecatedValue || warnedDeprecatedOpenAICompatible)
|
|
45
|
+
return;
|
|
46
|
+
warnedDeprecatedOpenAICompatible = true;
|
|
47
|
+
console.warn('[proxy] DEPRECATED: EVOMAP_OPENAI_COMPATIBLE_BASE_URLS / EVOLVER_OPENAI_COMPATIBLE_BASE_URLS is ignored for routing. ' +
|
|
48
|
+
'V2 accepts only https://*.api.openai.com/v1 via EVOLVER_LLM_OPENAI_BASE_URL, EVOMAP_OPENAI_BASE_URL, or OPENAI_BASE_URL. ' +
|
|
49
|
+
'LiteLLM / OpenRouter / Azure OpenAI-compatible hosts are not accepted on this OpenAI path — use a reverse proxy in front of api.openai.com or a non-OpenAI provider mode. ' +
|
|
50
|
+
'Remove the deprecated multi-base env var.');
|
|
51
|
+
}
|
|
52
|
+
/** Test helper: reset once-warn latch (unit tests only). */
|
|
53
|
+
function resetDeprecatedOpenAICompatibleWarning() {
|
|
54
|
+
warnedDeprecatedOpenAICompatible = false;
|
|
55
|
+
}
|
|
35
56
|
function isAllowedOpenAIHostname(hostname) {
|
|
36
57
|
const h = hostname.toLowerCase();
|
|
37
58
|
return h === 'api.openai.com' || h.endsWith('.api.openai.com');
|
|
@@ -57,6 +78,7 @@ function normalizeOpenAIBaseUrl(raw) {
|
|
|
57
78
|
return value;
|
|
58
79
|
}
|
|
59
80
|
export function resolveOpenAIUpstreamUrl(env = process.env) {
|
|
81
|
+
warnDeprecatedOpenAICompatible(env);
|
|
60
82
|
return normalizeOpenAIBaseUrl(env['EVOLVER_LLM_OPENAI_BASE_URL'] || env['EVOMAP_OPENAI_BASE_URL'] || env['OPENAI_BASE_URL'] || DEFAULT_OPENAI_UPSTREAM_URL);
|
|
61
83
|
}
|
|
62
84
|
function pathForOpenAIBase(path) {
|
|
@@ -488,4 +510,5 @@ export function makeVertexUpstream(opts = {}) {
|
|
|
488
510
|
const env = opts.env ?? process.env;
|
|
489
511
|
return fetchUpstream(`${baseUrl}${path}`, body, callOpts, buildVertexHeaders(env), fetchImpl, headersTimeoutMs, 'vertex', (headers, endpoint) => contentTypeIncludes(headers, 'text/event-stream') || /:streamGenerateContent(\b|\?|$)/.test(endpoint));
|
|
490
512
|
};
|
|
491
|
-
}
|
|
513
|
+
}
|
|
514
|
+
export { warnDeprecatedOpenAICompatible, resetDeprecatedOpenAICompatibleWarning };
|
package/dist/sync/engine.d.ts
CHANGED
|
@@ -23,6 +23,13 @@ export interface SyncEngineDeps {
|
|
|
23
23
|
onOutboundSucceeded?: (envelope: Envelope, result: unknown) => void | Promise<void>;
|
|
24
24
|
/** Cache a durable facade terminal result after SyncEngine moved the envelope to DLQ. */
|
|
25
25
|
onOutboundTerminal?: (envelope: Envelope, error: unknown) => void | Promise<void>;
|
|
26
|
+
/** Return the durable marker that proves a racing facade request already reached external acceptance. */
|
|
27
|
+
acceptedOutcomeKey?: (envelope: Envelope) => string | undefined;
|
|
28
|
+
/** Return a replayable terminal result that must be committed atomically with the mailbox DLQ transition. */
|
|
29
|
+
terminalOutcome?: (envelope: Envelope, error: unknown) => {
|
|
30
|
+
key: string;
|
|
31
|
+
result: unknown;
|
|
32
|
+
} | undefined;
|
|
26
33
|
/** Canonicalize an inbound envelope before durable insertion. */
|
|
27
34
|
normalizeInboundEnvelope?: (envelope: Envelope) => Envelope;
|
|
28
35
|
onOutboundFlushed?: (result: OutboundResult) => void | Promise<void>;
|
|
@@ -44,6 +51,7 @@ export interface InboundResult {
|
|
|
44
51
|
}
|
|
45
52
|
export declare class SyncEngine {
|
|
46
53
|
private readonly deps;
|
|
54
|
+
private readonly acceptedTaskOutboundMemory;
|
|
47
55
|
private lastActivityAt;
|
|
48
56
|
constructor(deps: SyncEngineDeps);
|
|
49
57
|
/** 出站: claim proxy 消息 → 经 proxyHandler 推 hub → complete; 终态(publish reject)直进 DLQ 不重试(money-safety). */
|
|
@@ -59,6 +67,10 @@ export declare class SyncEngine {
|
|
|
59
67
|
private notifyOutboundFlushed;
|
|
60
68
|
private normalizeOutbound;
|
|
61
69
|
private outboundDedupKey;
|
|
70
|
+
private acceptedTaskOutbound;
|
|
71
|
+
private durableAcceptedTaskOutbound;
|
|
72
|
+
private completeAcceptedOutcome;
|
|
73
|
+
private rememberAcceptedTaskOutbound;
|
|
62
74
|
private safeAck;
|
|
63
75
|
/** AgentEvent → core Envelope; 未知类型(不在目录)跳过. */
|
|
64
76
|
private toEnvelope;
|