@evomap/evolver-proxy 2.0.0-beta.0

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.
Files changed (82) hide show
  1. package/README.md +49 -0
  2. package/dist/bin/envFile.d.ts +10 -0
  3. package/dist/bin/envFile.js +68 -0
  4. package/dist/bin/evolver-llm-proxy.d.ts +2 -0
  5. package/dist/bin/evolver-llm-proxy.js +111 -0
  6. package/dist/bin/evolver-proxy.d.ts +83 -0
  7. package/dist/bin/evolver-proxy.js +511 -0
  8. package/dist/bin/proxySettings.d.ts +15 -0
  9. package/dist/bin/proxySettings.js +84 -0
  10. package/dist/bin/proxyStorePath.d.ts +1 -0
  11. package/dist/bin/proxyStorePath.js +16 -0
  12. package/dist/daemon/atpConsent.d.ts +13 -0
  13. package/dist/daemon/atpConsent.js +60 -0
  14. package/dist/daemon/ipcConfig.d.ts +1 -0
  15. package/dist/daemon/ipcConfig.js +13 -0
  16. package/dist/daemon/proxyDaemon.d.ts +191 -0
  17. package/dist/daemon/proxyDaemon.js +1015 -0
  18. package/dist/daemon/selectHub.d.ts +16 -0
  19. package/dist/daemon/selectHub.js +30 -0
  20. package/dist/index.d.ts +8 -0
  21. package/dist/index.js +8 -0
  22. package/dist/lifecycle/deployGuard.d.ts +46 -0
  23. package/dist/lifecycle/deployGuard.js +53 -0
  24. package/dist/lifecycle/legacyNodeId.d.ts +96 -0
  25. package/dist/lifecycle/legacyNodeId.js +163 -0
  26. package/dist/lifecycle/manager.d.ts +106 -0
  27. package/dist/lifecycle/manager.js +390 -0
  28. package/dist/llm/bodyCapture.d.ts +31 -0
  29. package/dist/llm/bodyCapture.js +293 -0
  30. package/dist/llm/index.d.ts +3 -0
  31. package/dist/llm/index.js +3 -0
  32. package/dist/llm/server.d.ts +35 -0
  33. package/dist/llm/server.js +359 -0
  34. package/dist/llm/traceBackfill.d.ts +53 -0
  35. package/dist/llm/traceBackfill.js +525 -0
  36. package/dist/llm/traceConfig.d.ts +7 -0
  37. package/dist/llm/traceConfig.js +44 -0
  38. package/dist/llm/traceControl.d.ts +16 -0
  39. package/dist/llm/traceControl.js +85 -0
  40. package/dist/llm/traceEnvelope.d.ts +75 -0
  41. package/dist/llm/traceEnvelope.js +286 -0
  42. package/dist/llm/traceSink.d.ts +61 -0
  43. package/dist/llm/traceSink.js +278 -0
  44. package/dist/llm/traceUploadPayload.d.ts +14 -0
  45. package/dist/llm/traceUploadPayload.js +27 -0
  46. package/dist/llm/upstream.d.ts +68 -0
  47. package/dist/llm/upstream.js +491 -0
  48. package/dist/private/adapterLoader.d.ts +46 -0
  49. package/dist/private/adapterLoader.js +62 -0
  50. package/dist/private/privateRuntimeSmokeOptions.d.ts +15 -0
  51. package/dist/private/privateRuntimeSmokeOptions.js +132 -0
  52. package/dist/router/cachePassthrough.d.ts +3 -0
  53. package/dist/router/cachePassthrough.js +13 -0
  54. package/dist/router/features.d.ts +9 -0
  55. package/dist/router/features.js +52 -0
  56. package/dist/router/index.d.ts +6 -0
  57. package/dist/router/index.js +6 -0
  58. package/dist/router/messagesRoute.d.ts +138 -0
  59. package/dist/router/messagesRoute.js +753 -0
  60. package/dist/router/modelRouter.d.ts +49 -0
  61. package/dist/router/modelRouter.js +66 -0
  62. package/dist/router/providerRoutes.d.ts +42 -0
  63. package/dist/router/providerRoutes.js +1579 -0
  64. package/dist/router/sseScan.d.ts +56 -0
  65. package/dist/router/sseScan.js +543 -0
  66. package/dist/selfUpdate/executor.d.ts +90 -0
  67. package/dist/selfUpdate/executor.js +179 -0
  68. package/dist/selfUpdate/failureCodes.d.ts +33 -0
  69. package/dist/selfUpdate/failureCodes.js +84 -0
  70. package/dist/selfUpdate/index.d.ts +5 -0
  71. package/dist/selfUpdate/index.js +5 -0
  72. package/dist/selfUpdate/lastUpdate.d.ts +43 -0
  73. package/dist/selfUpdate/lastUpdate.js +195 -0
  74. package/dist/selfUpdate/policy.d.ts +3 -0
  75. package/dist/selfUpdate/policy.js +9 -0
  76. package/dist/selfUpdate/releaseBinary.d.ts +56 -0
  77. package/dist/selfUpdate/releaseBinary.js +498 -0
  78. package/dist/selfUpdate/version.d.ts +2 -0
  79. package/dist/selfUpdate/version.js +14 -0
  80. package/dist/sync/engine.d.ts +65 -0
  81. package/dist/sync/engine.js +461 -0
  82. package/package.json +41 -0
@@ -0,0 +1,491 @@
1
+ // Real upstream for the LLM proxy handlers. Anthropic /v1/messages keeps the v1 token-mediation behavior;
2
+ // OpenAI-compatible routes use daemon-owned OpenAI credentials so the local proxy bearer token never goes
3
+ // upstream as provider auth.
4
+ //
5
+ // Header policy (token mediation, same as v1): forward ONLY x-api-key, anthropic-version and anthropic-*
6
+ // from the inbound request. Everything else — host, cookie, content-length and crucially `authorization`
7
+ // (consumed by the local server as proxy self-auth) — is dropped so the proxy token never leaks upstream.
8
+ // When the client sent no x-api-key, the proxy substitutes its own env credential per request (hot-swappable
9
+ // without restart): EVOMAP_ANTHROPIC_API_KEY / ANTHROPIC_API_KEY → x-api-key, else an upstream auth token.
10
+ import { ReadableStream } from 'node:stream/web';
11
+ import { canonicalizeForBedrock, resolveBedrockAliases, supportsAdaptiveThinking } from '../router/messagesRoute.js';
12
+ export const DEFAULT_UPSTREAM_URL = 'https://api.anthropic.com';
13
+ export const DEFAULT_OPENAI_UPSTREAM_URL = 'https://api.openai.com/v1';
14
+ export const DEFAULT_GEMINI_UPSTREAM_URL = 'https://generativelanguage.googleapis.com';
15
+ export const DEFAULT_OLLAMA_UPSTREAM_URL = 'http://127.0.0.1:11434';
16
+ /** Time allowed for upstream RESPONSE HEADERS to arrive. Never applied to the body: a healthy SSE stream
17
+ * routinely outlives any fixed deadline, so an AbortSignal.timeout-style cap on the whole fetch would kill
18
+ * long generations mid-stream (a real v1 hazard). Body lifetime is bounded by the client connection instead. */
19
+ export const DEFAULT_HEADERS_TIMEOUT_MS = 120_000;
20
+ let defaultBedrockRuntime = null;
21
+ async function loadDefaultBedrockRuntime() {
22
+ if (!defaultBedrockRuntime) {
23
+ const mod = await import('@aws-sdk/client-bedrock-runtime');
24
+ defaultBedrockRuntime = {
25
+ createClient: (args) => new mod.BedrockRuntimeClient(args),
26
+ createInvokeModelCommand: (input) => new mod.InvokeModelCommand(input),
27
+ createInvokeModelWithResponseStreamCommand: (input) => new mod.InvokeModelWithResponseStreamCommand(input),
28
+ };
29
+ }
30
+ return defaultBedrockRuntime;
31
+ }
32
+ function isOpenAiMode(upstreamMode) {
33
+ return upstreamMode === 'openai';
34
+ }
35
+ function isAllowedOpenAIHostname(hostname) {
36
+ const h = hostname.toLowerCase();
37
+ return h === 'api.openai.com' || h.endsWith('.api.openai.com');
38
+ }
39
+ function normalizeOpenAIBaseUrl(raw) {
40
+ const value = raw.replace(/\/+$/, '');
41
+ let parsed;
42
+ try {
43
+ parsed = new URL(value);
44
+ }
45
+ catch {
46
+ throw new Error('[proxy] OpenAI base URL is not a valid URL');
47
+ }
48
+ if (parsed.protocol !== 'https:'
49
+ || !isAllowedOpenAIHostname(parsed.hostname)
50
+ || parsed.pathname !== '/v1'
51
+ || parsed.username
52
+ || parsed.password
53
+ || parsed.search
54
+ || parsed.hash) {
55
+ throw new Error('[proxy] OpenAI base URL must be an OpenAI https://*.api.openai.com/v1 endpoint');
56
+ }
57
+ return value;
58
+ }
59
+ export function resolveOpenAIUpstreamUrl(env = process.env) {
60
+ return normalizeOpenAIBaseUrl(env['EVOLVER_LLM_OPENAI_BASE_URL'] || env['EVOMAP_OPENAI_BASE_URL'] || env['OPENAI_BASE_URL'] || DEFAULT_OPENAI_UPSTREAM_URL);
61
+ }
62
+ function pathForOpenAIBase(path) {
63
+ if (path === '/v1')
64
+ return '';
65
+ if (path.startsWith('/v1/'))
66
+ return path.slice('/v1'.length);
67
+ return path.startsWith('/') ? path : `/${path}`;
68
+ }
69
+ /**
70
+ * Validate an operator-configured upstream URL (#197): must parse as an http(s) URL with no embedded credentials.
71
+ * Scheme-only on purpose — NO host allowlist — so legit localhost (ollama) and internal gateways keep working,
72
+ * while file:/gopher:/data: schemes and userinfo-based SSRF tricks (user:pass@host) are refused. Returns trimmed.
73
+ */
74
+ function assertHttpUrl(raw, label) {
75
+ let parsed;
76
+ try {
77
+ parsed = new URL(raw);
78
+ }
79
+ catch {
80
+ throw new Error(`[proxy] ${label} is not a valid URL`);
81
+ }
82
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:')
83
+ throw new Error(`[proxy] ${label} must use http(s)`);
84
+ if (parsed.username || parsed.password)
85
+ throw new Error(`[proxy] ${label} must not embed credentials`);
86
+ return raw.replace(/\/+$/, '');
87
+ }
88
+ /** Resolve the upstream base URL. OpenAI-compatible routes never inherit the Anthropic-wide override. */
89
+ export function resolveUpstreamUrl(env = process.env, upstreamMode = 'anthropic') {
90
+ if (isOpenAiMode(upstreamMode))
91
+ return resolveOpenAIUpstreamUrl(env);
92
+ const raw = env['EVOLVER_LLM_UPSTREAM_URL'] || env['ANTHROPIC_BASE_URL'] || DEFAULT_UPSTREAM_URL;
93
+ return assertHttpUrl(raw, 'Anthropic upstream URL');
94
+ }
95
+ export function resolveGeminiUpstreamUrl(env = process.env) {
96
+ const raw = env['EVOMAP_GEMINI_BASE_URL'] || DEFAULT_GEMINI_UPSTREAM_URL;
97
+ return assertHttpUrl(raw, 'Gemini base URL');
98
+ }
99
+ export function resolveOllamaUpstreamUrl(env = process.env) {
100
+ const raw = env['EVOMAP_OLLAMA_BASE_URL'] || DEFAULT_OLLAMA_UPSTREAM_URL;
101
+ return assertHttpUrl(raw, 'Ollama base URL');
102
+ }
103
+ export function buildForwardHeaders(inbound, env, upstreamMode = 'anthropic') {
104
+ const fwd = { 'content-type': 'application/json' };
105
+ if (isOpenAiMode(upstreamMode)) {
106
+ for (const [k, v] of Object.entries(inbound)) {
107
+ if (v === undefined || v === null)
108
+ continue;
109
+ const lk = k.toLowerCase();
110
+ if (lk === 'openai-organization' || lk === 'openai-project' || lk === 'openai-beta' || lk.startsWith('x-stainless-') || lk === 'idempotency-key') {
111
+ fwd[lk] = String(v);
112
+ }
113
+ }
114
+ const apiKey = env['EVOLVER_LLM_OPENAI_API_KEY'] || env['EVOMAP_OPENAI_API_KEY'] || env['OPENAI_API_KEY'];
115
+ if (apiKey)
116
+ fwd['authorization'] = `Bearer ${apiKey}`;
117
+ }
118
+ else {
119
+ for (const [k, v] of Object.entries(inbound)) {
120
+ if (v === undefined || v === null)
121
+ continue;
122
+ const lk = k.toLowerCase();
123
+ if (lk === 'x-api-key' || lk === 'anthropic-version' || lk.startsWith('anthropic-'))
124
+ fwd[lk] = String(v);
125
+ }
126
+ if (!fwd['x-api-key']) {
127
+ if (env['EVOMAP_ANTHROPIC_API_KEY'])
128
+ fwd['x-api-key'] = env['EVOMAP_ANTHROPIC_API_KEY'];
129
+ else if (env['ANTHROPIC_API_KEY'])
130
+ fwd['x-api-key'] = env['ANTHROPIC_API_KEY'];
131
+ else if (env['EVOMAP_ANTHROPIC_AUTH_TOKEN'])
132
+ fwd['authorization'] = `Bearer ${env['EVOMAP_ANTHROPIC_AUTH_TOKEN']}`;
133
+ else if (env['EVOMAP_PROXY_AUTO_INJECTED'] !== '1' && env['ANTHROPIC_AUTH_TOKEN']) {
134
+ fwd['authorization'] = `Bearer ${env['ANTHROPIC_AUTH_TOKEN']}`;
135
+ }
136
+ }
137
+ }
138
+ return fwd;
139
+ }
140
+ function safeHeaderValue(value) {
141
+ if (value === undefined || value === null)
142
+ return null;
143
+ const s = String(value);
144
+ if (/[\r\n]/.test(s))
145
+ return null;
146
+ return s;
147
+ }
148
+ export function buildOpenAIHeaders(inbound, env) {
149
+ const fwd = { 'content-type': 'application/json' };
150
+ for (const [k, v] of Object.entries(inbound)) {
151
+ const lk = k.toLowerCase();
152
+ if (lk !== 'openai-organization' && lk !== 'openai-project' && lk !== 'openai-beta' && !lk.startsWith('x-stainless-'))
153
+ continue;
154
+ const hv = safeHeaderValue(v);
155
+ if (hv !== null)
156
+ fwd[lk] = hv;
157
+ }
158
+ const upstreamKey = env['EVOLVER_LLM_OPENAI_API_KEY'] || env['EVOMAP_OPENAI_API_KEY'] || env['OPENAI_API_KEY'];
159
+ if (!upstreamKey)
160
+ throw Object.assign(new Error('openai api key required'), { statusCode: 401 });
161
+ fwd.authorization = `Bearer ${upstreamKey}`;
162
+ return fwd;
163
+ }
164
+ export function buildGeminiHeaders(inbound, env) {
165
+ const fwd = { 'content-type': 'application/json' };
166
+ for (const [k, v] of Object.entries(inbound)) {
167
+ const lk = k.toLowerCase();
168
+ if (lk !== 'x-goog-user-project' && lk !== 'x-goog-api-client' && !lk.startsWith('x-goog-request-'))
169
+ continue;
170
+ const hv = safeHeaderValue(v);
171
+ if (hv !== null)
172
+ fwd[lk] = hv;
173
+ }
174
+ const upstreamKey = env['EVOMAP_GEMINI_API_KEY'] || env['GEMINI_API_KEY'] || env['GOOGLE_API_KEY'];
175
+ if (!upstreamKey)
176
+ throw Object.assign(new Error('gemini api key required'), { statusCode: 401 });
177
+ fwd['x-goog-api-key'] = upstreamKey;
178
+ return fwd;
179
+ }
180
+ export function buildOllamaHeaders(env) {
181
+ const fwd = { 'content-type': 'application/json' };
182
+ const upstreamKey = env['EVOMAP_OLLAMA_API_KEY'];
183
+ if (upstreamKey)
184
+ fwd.authorization = `Bearer ${upstreamKey}`;
185
+ return fwd;
186
+ }
187
+ export function buildVertexHeaders(env) {
188
+ const token = env['EVOMAP_VERTEX_ACCESS_TOKEN'];
189
+ if (!token)
190
+ throw Object.assign(new Error('vertex access token required'), { statusCode: 401 });
191
+ return { 'content-type': 'application/json', authorization: `Bearer ${token}` };
192
+ }
193
+ function providerGatewayError(provider, err, fallbackStatus = 502) {
194
+ const name = err && typeof err === 'object' && 'name' in err ? String(err.name) : '';
195
+ const isTimeout = name === 'TimeoutError' || name === 'AbortError';
196
+ return Object.assign(new Error(isTimeout ? `${provider} upstream timed out` : `${provider} upstream request failed`), {
197
+ statusCode: isTimeout ? 504 : fallbackStatus,
198
+ cause: err,
199
+ });
200
+ }
201
+ async function fetchUpstream(endpoint, body, opts, headers, fetchImpl, headersTimeoutMs, provider, streamDetector) {
202
+ const method = (opts.method || 'POST').toUpperCase();
203
+ const controller = new AbortController();
204
+ const timeoutErr = new Error(`${provider} upstream timed out`);
205
+ timeoutErr.name = 'TimeoutError';
206
+ const timer = setTimeout(() => controller.abort(timeoutErr), headersTimeoutMs);
207
+ let res;
208
+ try {
209
+ const init = { method, headers, signal: controller.signal };
210
+ if (method !== 'GET' && method !== 'HEAD')
211
+ init.body = JSON.stringify(body ?? {});
212
+ res = await fetchImpl(endpoint, init);
213
+ }
214
+ catch (err) {
215
+ clearTimeout(timer);
216
+ throw providerGatewayError(provider, err);
217
+ }
218
+ finally {
219
+ clearTimeout(timer);
220
+ }
221
+ const resHeaders = {};
222
+ for (const [k, v] of res.headers.entries())
223
+ resHeaders[k.toLowerCase()] = v;
224
+ const isStream = streamDetector(resHeaders, endpoint, body);
225
+ return {
226
+ status: res.status,
227
+ headers: resHeaders,
228
+ stream: isStream ? res.body : null,
229
+ text: isStream ? undefined : () => res.text().catch((err) => { throw providerGatewayError(provider, err); }),
230
+ };
231
+ }
232
+ const contentTypeIncludes = (headers, token) => (headers['content-type'] || '').toLowerCase().includes(token);
233
+ function jsonResult(status, body) {
234
+ const text = JSON.stringify(body);
235
+ return {
236
+ status,
237
+ headers: { 'content-type': 'application/json' },
238
+ stream: null,
239
+ text: () => text,
240
+ };
241
+ }
242
+ function bodyRecord(body) {
243
+ if (!body || typeof body !== 'object' || Array.isArray(body))
244
+ return {};
245
+ return body;
246
+ }
247
+ function textFromBytes(value) {
248
+ if (typeof value === 'string')
249
+ return value;
250
+ if (value instanceof Uint8Array)
251
+ return Buffer.from(value).toString('utf8');
252
+ if (value instanceof ArrayBuffer)
253
+ return Buffer.from(value).toString('utf8');
254
+ return '';
255
+ }
256
+ function bedrockErrorResult(err) {
257
+ const e = err && typeof err === 'object' ? err : {};
258
+ const metadata = e['$metadata'] && typeof e['$metadata'] === 'object' ? e['$metadata'] : {};
259
+ const name = typeof e['name'] === 'string' ? e['name'] : 'upstream_error';
260
+ const message = typeof e['message'] === 'string' ? e['message'] : String(err);
261
+ const httpStatus = typeof metadata['httpStatusCode'] === 'number' ? metadata['httpStatusCode'] : undefined;
262
+ const status = name === 'TimeoutError' || name === 'AbortError' ? 504 : httpStatus ?? 500;
263
+ return jsonResult(status, { type: 'error', error: { type: name, message } });
264
+ }
265
+ function normalizeBedrockBody(body, env) {
266
+ const source = bodyRecord(body);
267
+ const rawModel = typeof source['model'] === 'string' ? source['model'] : null;
268
+ const canonicalModel = rawModel ? canonicalizeForBedrock(rawModel, resolveBedrockAliases(env)) : null;
269
+ const modelId = typeof canonicalModel === 'string' && canonicalModel.length > 0 ? canonicalModel : null;
270
+ if (!modelId) {
271
+ return jsonResult(400, {
272
+ type: 'error',
273
+ error: { type: 'invalid_request_error', message: 'body.model required for Bedrock upstream' },
274
+ });
275
+ }
276
+ const upstreamBody = { ...source };
277
+ delete upstreamBody['model'];
278
+ if (!upstreamBody['anthropic_version'])
279
+ upstreamBody['anthropic_version'] = 'bedrock-2023-05-31';
280
+ const wantsStream = upstreamBody['stream'] === true;
281
+ delete upstreamBody['stream'];
282
+ const modelSupportsAdaptiveThinking = supportsAdaptiveThinking(modelId);
283
+ const thinking = upstreamBody['thinking'];
284
+ if (!modelSupportsAdaptiveThinking && thinking && typeof thinking === 'object' && !Array.isArray(thinking)) {
285
+ const thinkingRecord = thinking;
286
+ if (thinkingRecord['type'] === 'adaptive') {
287
+ const maxTokens = typeof upstreamBody['max_tokens'] === 'number' ? upstreamBody['max_tokens'] : 8192;
288
+ const budget = thinkingRecord['budget_tokens'];
289
+ // Legacy `enabled` thinking requires 1024 <= budget_tokens < max_tokens (Bedrock rejects anything else).
290
+ // If max_tokens leaves no room for a >=1024 budget, thinking cannot be enabled at all → disable, even when an
291
+ // inbound budget_tokens is present. Otherwise CLAMP the requested (or a derived) budget into [1024, maxTokens-1]
292
+ // so a sub-1024 or too-large inbound budget can't reach Bedrock verbatim and fail strict validation (Bugbot).
293
+ if (maxTokens <= 1024) {
294
+ upstreamBody['thinking'] = { type: 'disabled' };
295
+ }
296
+ else {
297
+ const desired = typeof budget === 'number' ? budget : Math.floor(maxTokens / 2);
298
+ const clamped = Math.min(Math.max(1024, desired), maxTokens - 1);
299
+ upstreamBody['thinking'] = { ...thinkingRecord, type: 'enabled', budget_tokens: clamped };
300
+ }
301
+ }
302
+ }
303
+ delete upstreamBody['context_management'];
304
+ if (!modelSupportsAdaptiveThinking)
305
+ delete upstreamBody['output_config'];
306
+ return { modelId, upstreamBody, wantsStream };
307
+ }
308
+ function bedrockException(event) {
309
+ return event.internalServerException
310
+ ?? event.modelStreamErrorException
311
+ ?? event.throttlingException
312
+ ?? event.validationException
313
+ ?? event.modelTimeoutException
314
+ ?? event.serviceUnavailableException
315
+ ?? null;
316
+ }
317
+ function bedrockChunkFrame(bytes) {
318
+ const data = textFromBytes(bytes);
319
+ if (!data)
320
+ return '';
321
+ try {
322
+ const parsed = JSON.parse(data);
323
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
324
+ const type = parsed['type'];
325
+ if (typeof type === 'string' && /^[A-Za-z0-9_.:-]+$/.test(type))
326
+ return `event: ${type}\ndata: ${data}\n\n`;
327
+ }
328
+ }
329
+ catch {
330
+ /* non-JSON chunks are still valid data-only SSE frames */
331
+ }
332
+ return `data: ${data}\n\n`;
333
+ }
334
+ function bedrockStreamToSse(body) {
335
+ const events = body && typeof body[Symbol.asyncIterator] === 'function'
336
+ ? body
337
+ : null;
338
+ return new ReadableStream({
339
+ async start(controller) {
340
+ if (!events) {
341
+ controller.close();
342
+ return;
343
+ }
344
+ try {
345
+ for await (const event of events) {
346
+ const bytes = event.chunk?.bytes;
347
+ if (bytes) {
348
+ controller.enqueue(Buffer.from(bedrockChunkFrame(bytes)));
349
+ continue;
350
+ }
351
+ const ex = bedrockException(event);
352
+ if (ex) {
353
+ const errFrame = JSON.stringify({
354
+ type: 'error',
355
+ error: {
356
+ type: typeof ex.name === 'string' ? ex.name : 'upstream_error',
357
+ message: typeof ex.message === 'string' ? ex.message : String(ex),
358
+ },
359
+ });
360
+ controller.enqueue(Buffer.from(`event: error\ndata: ${errFrame}\n\n`));
361
+ }
362
+ }
363
+ controller.close();
364
+ }
365
+ catch (err) {
366
+ controller.error(err);
367
+ }
368
+ },
369
+ cancel() {
370
+ try {
371
+ void events?.return?.();
372
+ }
373
+ catch {
374
+ /* async iterable already closed */
375
+ }
376
+ },
377
+ });
378
+ }
379
+ async function invokeBedrock(body, env, runtime, client, headersTimeoutMs) {
380
+ const normalized = normalizeBedrockBody(body, env);
381
+ if ('status' in normalized)
382
+ return normalized;
383
+ const input = {
384
+ modelId: normalized.modelId,
385
+ contentType: 'application/json',
386
+ accept: 'application/json',
387
+ body: JSON.stringify(normalized.upstreamBody),
388
+ };
389
+ const timeoutErr = new Error('bedrock upstream timed out');
390
+ timeoutErr.name = 'TimeoutError';
391
+ const abortController = new AbortController();
392
+ const abortTimer = setTimeout(() => abortController.abort(timeoutErr), headersTimeoutMs);
393
+ try {
394
+ if (normalized.wantsStream) {
395
+ const out = await client.send(runtime.createInvokeModelWithResponseStreamCommand(input), { abortSignal: abortController.signal });
396
+ clearTimeout(abortTimer);
397
+ const outRecord = out && typeof out === 'object' ? out : {};
398
+ return {
399
+ status: 200,
400
+ headers: { 'content-type': 'text/event-stream' },
401
+ stream: bedrockStreamToSse(outRecord['body']),
402
+ traceRequestBody: normalized.upstreamBody,
403
+ };
404
+ }
405
+ const out = await client.send(runtime.createInvokeModelCommand(input), { abortSignal: abortController.signal });
406
+ clearTimeout(abortTimer);
407
+ const outRecord = out && typeof out === 'object' ? out : {};
408
+ const text = textFromBytes(outRecord['body']);
409
+ return {
410
+ status: 200,
411
+ headers: { 'content-type': 'application/json' },
412
+ stream: null,
413
+ text: () => text,
414
+ traceRequestBody: normalized.upstreamBody,
415
+ };
416
+ }
417
+ catch (err) {
418
+ clearTimeout(abortTimer);
419
+ return { ...bedrockErrorResult(err), traceRequestBody: normalized.upstreamBody };
420
+ }
421
+ }
422
+ /** Build the production AnthropicProxy. Streaming is detected from the upstream content-type
423
+ * (text/event-stream → expose the response body stream; anything else → buffered text()). */
424
+ export function makeAnthropicUpstream(opts = {}) {
425
+ const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
426
+ const headersTimeoutMs = opts.headersTimeoutMs ?? DEFAULT_HEADERS_TIMEOUT_MS;
427
+ let bedrockClient = null;
428
+ let bedrockClientKey = '';
429
+ let bedrockClientRuntime = null;
430
+ const getBedrockClient = (env, runtime) => {
431
+ const args = {
432
+ region: env['AWS_REGION'] || env['AWS_DEFAULT_REGION'] || 'us-east-1',
433
+ ...(env['EVOMAP_BEDROCK_ENDPOINT'] ? { endpoint: assertHttpUrl(env['EVOMAP_BEDROCK_ENDPOINT'], 'Bedrock endpoint') } : {}),
434
+ };
435
+ const key = JSON.stringify(args);
436
+ if (!bedrockClient || bedrockClientKey !== key || bedrockClientRuntime !== runtime) {
437
+ bedrockClient = runtime.createClient(args);
438
+ bedrockClientKey = key;
439
+ bedrockClientRuntime = runtime;
440
+ }
441
+ return bedrockClient;
442
+ };
443
+ return async (path, body, callOpts) => {
444
+ const { inboundHeaders, upstreamMode } = callOpts;
445
+ if (upstreamMode === 'bedrock') {
446
+ const runtime = opts.bedrockRuntime ?? await loadDefaultBedrockRuntime();
447
+ const env = opts.env ?? process.env;
448
+ return invokeBedrock(body, env, runtime, getBedrockClient(env, runtime), headersTimeoutMs);
449
+ }
450
+ const env = opts.env ?? process.env;
451
+ return fetchUpstream(`${resolveUpstreamUrl(env, upstreamMode)}${isOpenAiMode(upstreamMode) ? pathForOpenAIBase(path) : path}`, body, callOpts, buildForwardHeaders(inboundHeaders, env, upstreamMode), fetchImpl, headersTimeoutMs, isOpenAiMode(upstreamMode) ? 'openai' : 'anthropic', (headers) => contentTypeIncludes(headers, 'text/event-stream'));
452
+ };
453
+ }
454
+ export function makeOpenAIUpstream(opts = {}) {
455
+ const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
456
+ const headersTimeoutMs = opts.headersTimeoutMs ?? DEFAULT_HEADERS_TIMEOUT_MS;
457
+ return async (path, body, callOpts) => {
458
+ const env = opts.env ?? process.env;
459
+ const baseUrl = callOpts.baseUrl ? normalizeOpenAIBaseUrl(callOpts.baseUrl) : resolveOpenAIUpstreamUrl(env);
460
+ return fetchUpstream(`${baseUrl}${path}`, body, callOpts, buildOpenAIHeaders(callOpts.inboundHeaders, env), fetchImpl, headersTimeoutMs, 'openai', (headers) => contentTypeIncludes(headers, 'text/event-stream'));
461
+ };
462
+ }
463
+ export function makeGeminiUpstream(opts = {}) {
464
+ const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
465
+ const headersTimeoutMs = opts.headersTimeoutMs ?? DEFAULT_HEADERS_TIMEOUT_MS;
466
+ return async (path, body, callOpts) => {
467
+ const env = opts.env ?? process.env;
468
+ const baseUrl = (callOpts.baseUrl || resolveGeminiUpstreamUrl(env)).replace(/\/+$/, '');
469
+ return fetchUpstream(`${baseUrl}${path}`, body, callOpts, buildGeminiHeaders(callOpts.inboundHeaders, env), fetchImpl, headersTimeoutMs, 'gemini', (headers, endpoint) => contentTypeIncludes(headers, 'text/event-stream') || /:streamGenerateContent(\b|\?|$)/.test(endpoint));
470
+ };
471
+ }
472
+ export function makeOllamaUpstream(opts = {}) {
473
+ const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
474
+ const headersTimeoutMs = opts.headersTimeoutMs ?? DEFAULT_HEADERS_TIMEOUT_MS;
475
+ return async (path, body, callOpts) => {
476
+ const env = opts.env ?? process.env;
477
+ const baseUrl = (callOpts.baseUrl || resolveOllamaUpstreamUrl(env)).replace(/\/+$/, '');
478
+ return fetchUpstream(`${baseUrl}${path}`, body, callOpts, buildOllamaHeaders(env), fetchImpl, headersTimeoutMs, 'ollama', (_headers, _endpoint, outboundBody) => !(outboundBody && typeof outboundBody === 'object' && outboundBody['stream'] === false));
479
+ };
480
+ }
481
+ export function makeVertexUpstream(opts = {}) {
482
+ const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
483
+ const headersTimeoutMs = opts.headersTimeoutMs ?? DEFAULT_HEADERS_TIMEOUT_MS;
484
+ return async (path, body, callOpts) => {
485
+ const baseUrl = (callOpts.baseUrl || '').replace(/\/+$/, '');
486
+ if (!baseUrl)
487
+ throw Object.assign(new Error('vertex base url required'), { statusCode: 500 });
488
+ const env = opts.env ?? process.env;
489
+ return fetchUpstream(`${baseUrl}${path}`, body, callOpts, buildVertexHeaders(env), fetchImpl, headersTimeoutMs, 'vertex', (headers, endpoint) => contentTypeIncludes(headers, 'text/event-stream') || /:streamGenerateContent(\b|\?|$)/.test(endpoint));
490
+ };
491
+ }
@@ -0,0 +1,46 @@
1
+ import type { hub as hubNs } from '@evomap/evolver-core';
2
+ import type { HelloResult, HeartbeatOptions, HeartbeatResult } from '../lifecycle/manager.js';
3
+ export type PrivateHubWithLifecycle = hubNs.HubCapability & {
4
+ hello(opts: {
5
+ rotate: boolean;
6
+ evolverVersion?: string;
7
+ }): Promise<HelloResult>;
8
+ heartbeat(opts?: HeartbeatOptions): Promise<HeartbeatResult>;
9
+ };
10
+ interface PrivateSsoExchange {
11
+ identity: () => {
12
+ subject: string;
13
+ claims?: Record<string, unknown>;
14
+ };
15
+ exchange: (identity: {
16
+ subject: string;
17
+ claims?: Record<string, unknown>;
18
+ }) => Promise<{
19
+ token: string;
20
+ expiresInMs?: number;
21
+ }>;
22
+ now?: () => number;
23
+ }
24
+ export interface ConnectPrivateHubOptions {
25
+ hubUrl: string;
26
+ sso: PrivateSsoExchange;
27
+ senderId: () => string | undefined;
28
+ env?: Record<string, string | undefined>;
29
+ now?: () => number;
30
+ }
31
+ type DynamicImporter = (specifier: string) => Promise<unknown>;
32
+ export interface PrivateProxyHubRuntime {
33
+ hub: PrivateHubWithLifecycle;
34
+ auth: hubNs.AuthProvider;
35
+ }
36
+ export interface ConnectPrivateProxyHubOptions {
37
+ hubUrl: string;
38
+ senderId: () => string | undefined;
39
+ env: Record<string, string | undefined>;
40
+ now?: () => number;
41
+ importer?: DynamicImporter;
42
+ }
43
+ export declare function resolvePrivateEnterpriseToken(env: Record<string, string | undefined>): string | undefined;
44
+ export declare function resolvePrivateEnterpriseSubject(env: Record<string, string | undefined>): string;
45
+ export declare function connectPrivateProxyHub(opts: ConnectPrivateProxyHubOptions): Promise<PrivateProxyHubRuntime>;
46
+ export {};
@@ -0,0 +1,62 @@
1
+ const DEFAULT_PRIVATE_ADAPTER_MODULE = '@evomap/evolver-adapter-private';
2
+ export function resolvePrivateEnterpriseToken(env) {
3
+ return firstEnv(env, 'EVOMAP_ENTERPRISE_TOKEN', 'EVOMAP_PRIVATE_HUB_TOKEN', 'PHUB_ENTERPRISE_TOKEN', 'PRIVATE_HUB_ENTERPRISE_TOKEN');
4
+ }
5
+ export function resolvePrivateEnterpriseSubject(env) {
6
+ return firstEnv(env, 'EVOMAP_ENTERPRISE_SUBJECT', 'EVOMAP_PRIVATE_SUBJECT', 'PHUB_ENTERPRISE_SUBJECT', 'USER') ?? 'evolver-proxy';
7
+ }
8
+ export async function connectPrivateProxyHub(opts) {
9
+ const token = resolvePrivateEnterpriseToken(opts.env);
10
+ if (!token) {
11
+ throw new Error('EVOMAP_HUB_MODE=private 需要 EVOMAP_ENTERPRISE_TOKEN(也兼容 EVOMAP_PRIVATE_HUB_TOKEN / PHUB_ENTERPRISE_TOKEN)');
12
+ }
13
+ const moduleName = opts.env['EVOMAP_PRIVATE_ADAPTER_MODULE']?.trim() || DEFAULT_PRIVATE_ADAPTER_MODULE;
14
+ const connectPrivateHub = await loadConnectPrivateHub(moduleName, opts.importer ?? ((specifier) => import(specifier)));
15
+ const now = opts.now ?? (() => Date.now());
16
+ const subject = resolvePrivateEnterpriseSubject(opts.env);
17
+ const { hub, auth } = connectPrivateHub({
18
+ hubUrl: opts.hubUrl,
19
+ senderId: opts.senderId,
20
+ env: opts.env,
21
+ now,
22
+ sso: {
23
+ identity: () => ({ subject }),
24
+ exchange: async () => ({ token }),
25
+ now,
26
+ },
27
+ });
28
+ assertPrivateLifecycle(hub, moduleName);
29
+ return { hub, auth };
30
+ }
31
+ async function loadConnectPrivateHub(moduleName, importer) {
32
+ let loaded;
33
+ try {
34
+ loaded = await importer(moduleName);
35
+ }
36
+ catch (err) {
37
+ throw new Error(`EVOMAP_HUB_MODE=private 需要安装/链接 ${moduleName}: ${err instanceof Error ? err.message : String(err)}`);
38
+ }
39
+ const mod = asRecord(loaded);
40
+ const connect = mod?.['connectPrivateHub'];
41
+ if (typeof connect !== 'function') {
42
+ throw new Error(`${moduleName} 未导出 connectPrivateHub,无法装配 private hub runtime`);
43
+ }
44
+ return connect;
45
+ }
46
+ function assertPrivateLifecycle(hub, moduleName) {
47
+ const candidate = asRecord(hub);
48
+ if (typeof candidate?.['hello'] !== 'function' || typeof candidate['heartbeat'] !== 'function') {
49
+ throw new Error(`${moduleName} 的 hub 缺少 hello/heartbeat lifecycle 方法,无法接入 evolver-proxy`);
50
+ }
51
+ }
52
+ function firstEnv(env, ...keys) {
53
+ for (const key of keys) {
54
+ const value = env[key]?.trim();
55
+ if (value)
56
+ return value;
57
+ }
58
+ return undefined;
59
+ }
60
+ function asRecord(value) {
61
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
62
+ }
@@ -0,0 +1,15 @@
1
+ export interface PrivateRuntimeSmokeOptions {
2
+ env: Record<string, string | undefined>;
3
+ run: boolean;
4
+ runSearch: boolean;
5
+ runReuseResult: boolean;
6
+ runPublish: boolean;
7
+ }
8
+ export interface PrivateRuntimeSmokeResolveDeps {
9
+ configRoot?: string;
10
+ exists?: (path: string) => boolean;
11
+ readConfigFile?: (path: string) => string;
12
+ readEnvFile?: (path: string) => string;
13
+ statMode?: (path: string) => number | undefined;
14
+ }
15
+ export declare function resolvePrivateRuntimeSmokeOptions(sourceEnv?: Record<string, string | undefined>, readEnvFileOrDeps?: ((path: string) => string) | PrivateRuntimeSmokeResolveDeps): PrivateRuntimeSmokeOptions;