@evomap/evolver 1.89.7 → 1.89.9

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 (59) hide show
  1. package/conformance/savings-core/constants.json +30 -0
  2. package/conformance/savings-core/golden-vectors.json +333 -0
  3. package/package.json +2 -1
  4. package/src/evolve/guards.js +1 -1
  5. package/src/evolve/pipeline/collect.js +1 -1
  6. package/src/evolve/pipeline/dispatch.js +1 -1
  7. package/src/evolve/pipeline/enrich.js +1 -1
  8. package/src/evolve/pipeline/hub.js +1 -1
  9. package/src/evolve/pipeline/select.js +1 -1
  10. package/src/evolve/pipeline/signals.js +1 -1
  11. package/src/evolve/utils.js +1 -1
  12. package/src/evolve.js +1 -1
  13. package/src/gep/a2aProtocol.js +1 -1
  14. package/src/gep/antiAbuseTelemetry.js +1 -1
  15. package/src/gep/autoDistillConv.js +1 -1
  16. package/src/gep/autoDistillLlm.js +1 -1
  17. package/src/gep/candidateEval.js +1 -1
  18. package/src/gep/candidates.js +1 -1
  19. package/src/gep/contentHash.js +1 -1
  20. package/src/gep/conversationSniffer.js +1 -1
  21. package/src/gep/crypto.js +1 -1
  22. package/src/gep/curriculum.js +1 -1
  23. package/src/gep/deviceId.js +1 -1
  24. package/src/gep/envFingerprint.js +1 -1
  25. package/src/gep/epigenetics.js +1 -1
  26. package/src/gep/execBridge.js +1 -1
  27. package/src/gep/explore.js +1 -1
  28. package/src/gep/hash.js +1 -1
  29. package/src/gep/hubFetch.js +1 -1
  30. package/src/gep/hubReview.js +1 -1
  31. package/src/gep/hubSearch.js +1 -1
  32. package/src/gep/hubVerify.js +1 -1
  33. package/src/gep/learningSignals.js +1 -1
  34. package/src/gep/memoryGraph.js +1 -1
  35. package/src/gep/memoryGraphAdapter.js +1 -1
  36. package/src/gep/mutation.js +1 -1
  37. package/src/gep/narrativeMemory.js +1 -1
  38. package/src/gep/openPRRegistry.js +1 -1
  39. package/src/gep/personality.js +1 -1
  40. package/src/gep/policyCheck.js +1 -1
  41. package/src/gep/prompt.js +1 -1
  42. package/src/gep/recallInject.js +1 -1
  43. package/src/gep/recallVerifier.js +1 -1
  44. package/src/gep/reflection.js +1 -1
  45. package/src/gep/savingsCore.js +1 -1
  46. package/src/gep/selector.js +1 -1
  47. package/src/gep/skillDistiller.js +1 -1
  48. package/src/gep/solidify.js +1 -1
  49. package/src/gep/strategy.js +1 -1
  50. package/src/gep/tokenSavings.js +1 -1
  51. package/src/gep/workspaceKeychain.js +1 -1
  52. package/src/proxy/extensions/traceControl.js +1 -1
  53. package/src/proxy/index.js +101 -1
  54. package/src/proxy/inject.js +1 -1
  55. package/src/proxy/router/gemini_route.js +154 -0
  56. package/src/proxy/router/responses_route.js +14 -3
  57. package/src/proxy/server/routes.js +11 -0
  58. package/src/proxy/trace/extractor.js +1 -1
  59. package/src/proxy/trace/usage.js +1 -1
@@ -0,0 +1,154 @@
1
+ 'use strict';
2
+
3
+ // Gemini passthrough handler (format-aware routing, NO translation). A Gemini-shaped request — Google's native
4
+ // `/v1beta/models/<model>:generateContent` | `:streamGenerateContent` path, body `{contents, generationConfig,
5
+ // systemInstruction, tools}` — is forwarded verbatim to the Gemini upstream. The model + action live in the
6
+ // PATH (not the body), so we reconstruct the path (+ query like ?alt=sse) and pass it through. Trace capture
7
+ // mirrors the other providers (usage/finish/stream tee). Point the Gemini CLI/SDK's base URL at the proxy and
8
+ // it works unmodified — no Anthropic/OpenAI conversion (lossy translation is deliberately avoided).
9
+
10
+ const { createProxyTrace } = require('../trace/extractor');
11
+
12
+ const GEMINI_RESPONSE_HEADER_ALLOWLIST = new Set([
13
+ 'content-type',
14
+ 'retry-after',
15
+ 'x-request-id',
16
+ ]);
17
+
18
+ function hasGeminiUpstreamCredential() {
19
+ return !!(process.env.EVOMAP_GEMINI_API_KEY || process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY);
20
+ }
21
+
22
+ function upstreamStatus(err, fallback = 502) {
23
+ const status = Number(err && err.statusCode);
24
+ return Number.isFinite(status) ? status : fallback;
25
+ }
26
+
27
+ function asUpstreamError(err, fallback = 502) {
28
+ if (err && err.statusCode && /^gemini upstream /.test(err.message || '')) return err;
29
+ const out = new Error('gemini upstream request failed');
30
+ out.statusCode = upstreamStatus(err, fallback);
31
+ out.cause = err;
32
+ return out;
33
+ }
34
+
35
+ function responseToBody(raw, status, headers, log) {
36
+ if (!raw) return {};
37
+ try {
38
+ return JSON.parse(raw);
39
+ } catch {
40
+ log.warn?.(JSON.stringify({
41
+ event: 'gemini_fallback',
42
+ reason: 'upstream_non_json',
43
+ upstream_status: status,
44
+ content_type: (headers && headers['content-type']) || '',
45
+ response_bytes: Buffer.byteLength(raw),
46
+ }));
47
+ return { error: raw };
48
+ }
49
+ }
50
+
51
+ function copyGeminiResponseHeaders(headers = {}) {
52
+ const out = {};
53
+ for (const [name, value] of Object.entries(headers || {})) {
54
+ const lower = String(name || '').toLowerCase();
55
+ if (!GEMINI_RESPONSE_HEADER_ALLOWLIST.has(lower) && !lower.startsWith('x-goog-')) continue;
56
+ if (value === undefined || value === null) continue;
57
+ const headerValue = Array.isArray(value) ? value.join(', ') : String(value);
58
+ if (/[\r\n]/.test(headerValue)) continue;
59
+ out[lower] = headerValue;
60
+ }
61
+ return out;
62
+ }
63
+
64
+ // `<model>:<action>` — the model can contain dots/dashes; the action is the part after the LAST colon
65
+ // (generateContent | streamGenerateContent | countTokens | ...). Returns {model, action} (action '' if absent).
66
+ function parseModelAction(modelAction) {
67
+ const s = String(modelAction || '');
68
+ const idx = s.lastIndexOf(':');
69
+ if (idx === -1) return { model: s, action: '' };
70
+ return { model: s.slice(0, idx), action: s.slice(idx + 1) };
71
+ }
72
+
73
+ function buildGeminiHandler({ geminiProxy, logger, traceStore, onTraceQueued } = {}) {
74
+ if (typeof geminiProxy !== 'function') {
75
+ throw new Error('buildGeminiHandler requires geminiProxy(path, body, opts)');
76
+ }
77
+ const log = logger || console;
78
+
79
+ return async ({ body, headers, params, query }) => {
80
+ const inboundHeaders = headers || {};
81
+ if (!hasGeminiUpstreamCredential()) {
82
+ throw Object.assign(new Error('gemini api key required'), { statusCode: 401 });
83
+ }
84
+
85
+ const modelAction = (params && params.modelAction) || '';
86
+ const { model, action } = parseModelAction(modelAction);
87
+ // Reconstruct the native Gemini path + query (e.g. ?alt=sse for streaming) and forward verbatim.
88
+ const qs = query && Object.keys(query).length ? '?' + new URLSearchParams(query).toString() : '';
89
+ const reqPath = `/v1beta/models/${modelAction}${qs}`;
90
+
91
+ let trace = null;
92
+ try {
93
+ trace = createProxyTrace({
94
+ route: `POST /v1beta/models/${modelAction}`,
95
+ headers: inboundHeaders,
96
+ body,
97
+ upstreamMode: 'gemini',
98
+ originalModel: model,
99
+ chosenModel: model,
100
+ store: traceStore,
101
+ logger: traceStore ? log : null,
102
+ onTraceQueued,
103
+ });
104
+ } catch (_) { /* best-effort trace; never break the request */ }
105
+
106
+ let upstream;
107
+ try {
108
+ upstream = await geminiProxy(reqPath, body, { inboundHeaders, upstreamMode: 'gemini' });
109
+ } catch (err) {
110
+ const wrapped = asUpstreamError(err, upstreamStatus(err));
111
+ trace?.record({ status: wrapped.statusCode, error: wrapped, upstreamMode: 'gemini', model });
112
+ throw wrapped;
113
+ }
114
+
115
+ if (upstream.stream) {
116
+ const forwardHeaders = copyGeminiResponseHeaders(upstream.headers);
117
+ const ct = upstream.headers && upstream.headers['content-type'];
118
+ if (ct) forwardHeaders['Content-Type'] = ct;
119
+ trace?.recordStreamStart({ status: upstream.status, upstreamMode: 'gemini', model, headers: forwardHeaders });
120
+ return {
121
+ status: upstream.status,
122
+ // Tee the Gemini SSE body so the deferred trace captures usageMetadata + finishReason. Bytes unchanged.
123
+ stream: trace ? trace.observeStream(upstream.stream) : upstream.stream,
124
+ headers: forwardHeaders,
125
+ };
126
+ }
127
+
128
+ let raw = '';
129
+ if (upstream.text) {
130
+ try {
131
+ raw = await upstream.text();
132
+ } catch (err) {
133
+ const wrapped = asUpstreamError(err, upstreamStatus(err));
134
+ trace?.record({ status: wrapped.statusCode, error: wrapped, upstreamMode: 'gemini', model });
135
+ throw wrapped;
136
+ }
137
+ }
138
+ const respBody = responseToBody(raw, upstream.status, upstream.headers, log);
139
+ trace?.record({ status: upstream.status, responseBody: respBody, upstreamMode: 'gemini', model, headers: upstream.headers });
140
+ return {
141
+ status: upstream.status,
142
+ body: respBody,
143
+ headers: copyGeminiResponseHeaders(upstream.headers),
144
+ };
145
+ };
146
+ }
147
+
148
+ module.exports = {
149
+ buildGeminiHandler,
150
+ copyGeminiResponseHeaders,
151
+ hasGeminiUpstreamCredential,
152
+ responseToBody,
153
+ parseModelAction,
154
+ };
@@ -66,7 +66,11 @@ function copyOpenAIResponseHeaders(headers = {}) {
66
66
  return out;
67
67
  }
68
68
 
69
- function buildResponsesHandler({ openAIProxy, logger, traceStore, onTraceQueued } = {}) {
69
+ // Generic OpenAI passthrough handler. `upstreamPath` selects the OpenAI endpoint (/responses for codex's
70
+ // Responses API, /chat/completions for the Chat Completions API used by cursor's OpenAI mode + generic OpenAI
71
+ // clients). Both share the same upstream, auth, header allow-list, trace, and stream tee — the only difference
72
+ // is the path + the trace route label. No translation: each OpenAI dialect goes to its native OpenAI endpoint.
73
+ function buildResponsesHandler({ openAIProxy, logger, traceStore, onTraceQueued, upstreamPath = '/responses', traceRoute = 'POST /v1/responses' } = {}) {
70
74
  if (typeof openAIProxy !== 'function') {
71
75
  throw new Error('buildResponsesHandler requires openAIProxy(path, body, opts)');
72
76
  }
@@ -82,7 +86,7 @@ function buildResponsesHandler({ openAIProxy, logger, traceStore, onTraceQueued
82
86
  let trace = null;
83
87
  try {
84
88
  trace = createProxyTrace({
85
- route: 'POST /v1/responses',
89
+ route: traceRoute,
86
90
  headers: inboundHeaders,
87
91
  body,
88
92
  upstreamMode: 'openai',
@@ -96,7 +100,7 @@ function buildResponsesHandler({ openAIProxy, logger, traceStore, onTraceQueued
96
100
 
97
101
  let upstream;
98
102
  try {
99
- upstream = await openAIProxy('/responses', body, {
103
+ upstream = await openAIProxy(upstreamPath, body, {
100
104
  inboundHeaders,
101
105
  upstreamMode: 'openai',
102
106
  });
@@ -151,8 +155,15 @@ function buildResponsesHandler({ openAIProxy, logger, traceStore, onTraceQueued
151
155
  };
152
156
  }
153
157
 
158
+ // OpenAI Chat Completions ingress (cursor's OpenAI mode + generic OpenAI clients). Same OpenAI upstream as the
159
+ // Responses handler, just the /chat/completions endpoint — point an OpenAI-Chat client's base URL at the proxy.
160
+ function buildChatCompletionsHandler(opts = {}) {
161
+ return buildResponsesHandler({ ...opts, upstreamPath: '/chat/completions', traceRoute: 'POST /v1/chat/completions' });
162
+ }
163
+
154
164
  module.exports = {
155
165
  buildResponsesHandler,
166
+ buildChatCompletionsHandler,
156
167
  copyOpenAIResponseHeaders,
157
168
  hasOpenAIUpstreamCredential,
158
169
  responseToBody,
@@ -10,6 +10,8 @@ function buildRoutes(store, proxyHandlers, taskMonitor, extensions) {
10
10
  sessionHandler,
11
11
  messagesHandler,
12
12
  responsesHandler,
13
+ geminiHandler,
14
+ chatCompletionsHandler,
13
15
  } = extensions || {};
14
16
  const routes = {
15
17
  // -- Mailbox --
@@ -476,6 +478,15 @@ function buildRoutes(store, proxyHandlers, taskMonitor, extensions) {
476
478
  if (responsesHandler) {
477
479
  routes['POST /v1/responses'] = responsesHandler;
478
480
  }
481
+ if (geminiHandler) {
482
+ // Native Gemini path: model + action (generateContent | streamGenerateContent) are one path segment
483
+ // (`<model>:<action>`), matched as :modelAction and split by the handler.
484
+ routes['POST /v1beta/models/:modelAction'] = geminiHandler;
485
+ }
486
+ if (chatCompletionsHandler) {
487
+ // OpenAI Chat Completions ingress (cursor's OpenAI mode + generic OpenAI clients) → OpenAI upstream.
488
+ routes['POST /v1/chat/completions'] = chatCompletionsHandler;
489
+ }
479
490
 
480
491
  return routes;
481
492
  }