@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,56 @@
1
+ export interface SseUsage {
2
+ input_tokens?: number;
3
+ output_tokens?: number;
4
+ cache_creation_input_tokens?: number;
5
+ cache_read_input_tokens?: number;
6
+ }
7
+ export interface SseScanResult {
8
+ usage?: SseUsage;
9
+ stop_reason?: string | null;
10
+ response_id?: string;
11
+ error?: string;
12
+ content_text?: string;
13
+ content_events?: unknown[];
14
+ semantic_tail_events?: unknown[];
15
+ raw_stream_body?: string;
16
+ raw_stream_truncated?: boolean;
17
+ content_truncated?: boolean;
18
+ dropped_event_count?: number;
19
+ }
20
+ export interface StreamEndInfo {
21
+ error?: string;
22
+ cancelled?: boolean;
23
+ }
24
+ /** Partial-line buffer cap: a pathological stream with no newlines must not grow memory unbounded. OpenAI
25
+ * Responses `response.completed` can put the full response onto one data line, so keep this generous. */
26
+ export declare const MAX_PARTIAL_LINE_BYTES: number;
27
+ export declare class SseUsageScanner {
28
+ readonly result: SseScanResult;
29
+ private buf;
30
+ private overCapLine;
31
+ private largeLineTail;
32
+ private rawStreamChars;
33
+ private readonly decoder;
34
+ /** OPT-IN: when true, accumulate streamed completion text into result.content_text (bounded). Default false
35
+ * keeps the scanner metadata-only. Driven by the body-capture flag (see bodyCapture.ts). */
36
+ private readonly captureContent;
37
+ private readonly limits;
38
+ private readonly maxPartialLineBytes;
39
+ constructor(opts?: {
40
+ captureContent?: boolean;
41
+ env?: NodeJS.ProcessEnv;
42
+ });
43
+ push(chunk: unknown): void;
44
+ finish(): void;
45
+ private captureRawStream;
46
+ private pushText;
47
+ private scanLargeLineText;
48
+ private markDroppedDataLine;
49
+ private scanLine;
50
+ }
51
+ /**
52
+ * Wrap a stream so every chunk passes to `push` before reaching the consumer, with `onEnd` fired exactly once
53
+ * on completion, error, or client cancel. Handles the two shapes _streamResponse relays (Web ReadableStream
54
+ * and async iterables); anything opaque is returned untouched with an immediate onEnd (trace without usage).
55
+ */
56
+ export declare function teeStreamForScan(stream: unknown, push: (chunk: unknown) => void, onEnd: (info?: StreamEndInfo) => void): unknown;
@@ -0,0 +1,543 @@
1
+ // Passive SSE observer for provider streaming paths. The relay must stay byte-transparent (the client owns the
2
+ // SSE framing), so trace capture tees the stream: every chunk is forwarded untouched while a scanner accumulates
3
+ // just enough text to read provider-neutral metadata events. Parse failures are silently ignored: a trace without
4
+ // usage beats a broken relay.
5
+ import { ReadableStream } from 'node:stream/web';
6
+ import { bodyMaxChars, DEFAULT_TRACE_ENVELOPE_MAX_CHARS, positiveIntegerFromEnv, redactText, } from '../llm/bodyCapture.js';
7
+ /** Caps for OPT-IN streamed-content accumulation (see SseUsageScanner captureContent). Defaults align with the
8
+ * trace body/envelope cap so normal Hub-sized streams are captured fully; env overrides can lower/raise them. */
9
+ const DEFAULT_STREAM_MAX_EVENTS = 100_000;
10
+ function sseCaptureLimits(env = process.env) {
11
+ const bodyMax = bodyMaxChars(env);
12
+ return {
13
+ contentMaxChars: positiveIntegerFromEnv(env, ['EVOLVER_LLM_TRACE_STREAM_CONTENT_MAX_CHARS', 'EVOMAP_PROXY_TRACE_STREAM_CONTENT_MAX_CHARS'], bodyMax),
14
+ contentMaxEvents: positiveIntegerFromEnv(env, ['EVOLVER_LLM_TRACE_STREAM_MAX_EVENTS', 'EVOMAP_PROXY_TRACE_STREAM_MAX_EVENTS'], DEFAULT_STREAM_MAX_EVENTS),
15
+ semanticTailMaxEvents: positiveIntegerFromEnv(env, ['EVOLVER_LLM_TRACE_STREAM_SEMANTIC_TAIL_MAX_EVENTS', 'EVOMAP_PROXY_TRACE_STREAM_SEMANTIC_TAIL_MAX_EVENTS'], DEFAULT_STREAM_MAX_EVENTS),
16
+ rawStreamMaxChars: positiveIntegerFromEnv(env, ['EVOLVER_LLM_TRACE_STREAM_RAW_MAX_CHARS', 'EVOMAP_PROXY_TRACE_STREAM_RAW_MAX_CHARS'], bodyMax),
17
+ };
18
+ }
19
+ /** Append a streamed text delta to the running content buffer, bounded by the configured capture cap. */
20
+ function appendContent(into, piece, limits) {
21
+ if (typeof piece !== 'string' || piece.length === 0)
22
+ return;
23
+ const cur = into.content_text ?? '';
24
+ if (cur.length >= limits.contentMaxChars) {
25
+ into.content_truncated = true;
26
+ return;
27
+ }
28
+ const next = cur + piece;
29
+ if (next.length > limits.contentMaxChars)
30
+ into.content_truncated = true;
31
+ into.content_text = next.slice(0, limits.contentMaxChars);
32
+ }
33
+ function appendEvent(into, evt, limits) {
34
+ const events = into.content_events ?? [];
35
+ if (events.length >= limits.contentMaxEvents) {
36
+ into.dropped_event_count = (into.dropped_event_count ?? 0) + 1;
37
+ appendSemanticTailEvent(into, evt, limits);
38
+ return;
39
+ }
40
+ events.push(evt);
41
+ into.content_events = events;
42
+ }
43
+ function isRecord(value) {
44
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
45
+ }
46
+ function hasUsage(value) {
47
+ return isRecord(value) && Object.keys(value).length > 0;
48
+ }
49
+ function isSemanticToolType(value) {
50
+ return value === 'function_call' || value === 'tool_use' || value === 'function_call_output' || value === 'tool_result';
51
+ }
52
+ function compactSemanticChoice(choice) {
53
+ if (!isRecord(choice))
54
+ return null;
55
+ const out = {};
56
+ if (choice['index'] !== undefined)
57
+ out['index'] = choice['index'];
58
+ if (choice['finish_reason'] !== undefined)
59
+ out['finish_reason'] = choice['finish_reason'];
60
+ const delta = isRecord(choice['delta']) ? choice['delta'] : undefined;
61
+ if (delta) {
62
+ const compactDelta = {};
63
+ if (Array.isArray(delta['tool_calls']))
64
+ compactDelta['tool_calls'] = delta['tool_calls'];
65
+ if (isRecord(delta['function_call']))
66
+ compactDelta['function_call'] = delta['function_call'];
67
+ if (Object.keys(compactDelta).length > 0)
68
+ out['delta'] = compactDelta;
69
+ }
70
+ const message = isRecord(choice['message']) ? choice['message'] : undefined;
71
+ if (message) {
72
+ const compactMessage = {};
73
+ if (Array.isArray(message['tool_calls']))
74
+ compactMessage['tool_calls'] = message['tool_calls'];
75
+ if (isRecord(message['function_call']))
76
+ compactMessage['function_call'] = message['function_call'];
77
+ if (Object.keys(compactMessage).length > 0)
78
+ out['message'] = compactMessage;
79
+ }
80
+ return Object.keys(out).length > 0 ? out : null;
81
+ }
82
+ function compactToolItems(value) {
83
+ if (!Array.isArray(value))
84
+ return [];
85
+ return value.filter((item) => isRecord(item) && isSemanticToolType(item['type']));
86
+ }
87
+ function compactStreamEvent(evt) {
88
+ if (!isRecord(evt))
89
+ return null;
90
+ const out = {};
91
+ for (const key of ['type', 'id', 'responseId', 'item_id', 'output_index', 'call_id']) {
92
+ if (evt[key] !== undefined)
93
+ out[key] = evt[key];
94
+ }
95
+ if (hasUsage(evt['usage']))
96
+ out['usage'] = evt['usage'];
97
+ if (hasUsage(evt['usageMetadata']))
98
+ out['usageMetadata'] = evt['usageMetadata'];
99
+ if (Array.isArray(evt['choices'])) {
100
+ const choices = evt['choices'].map(compactSemanticChoice).filter((choice) => choice !== null);
101
+ if (choices.length > 0)
102
+ out['choices'] = choices;
103
+ }
104
+ const message = isRecord(evt['message']) ? evt['message'] : undefined;
105
+ if (message) {
106
+ const compactMessage = {};
107
+ if (message['id'] !== undefined)
108
+ compactMessage['id'] = message['id'];
109
+ if (hasUsage(message['usage']))
110
+ compactMessage['usage'] = message['usage'];
111
+ if (Array.isArray(message['tool_calls']))
112
+ compactMessage['tool_calls'] = message['tool_calls'];
113
+ if (isRecord(message['function_call']))
114
+ compactMessage['function_call'] = message['function_call'];
115
+ if (Object.keys(compactMessage).length > 0)
116
+ out['message'] = compactMessage;
117
+ }
118
+ const response = isRecord(evt['response']) ? evt['response'] : undefined;
119
+ if (response) {
120
+ const compactResponse = {};
121
+ if (response['id'] !== undefined)
122
+ compactResponse['id'] = response['id'];
123
+ if (response['status'] !== undefined)
124
+ compactResponse['status'] = response['status'];
125
+ if (response['incomplete_details'] !== undefined)
126
+ compactResponse['incomplete_details'] = response['incomplete_details'];
127
+ if (hasUsage(response['usage']))
128
+ compactResponse['usage'] = response['usage'];
129
+ const output = compactToolItems(response['output']);
130
+ if (output.length > 0)
131
+ compactResponse['output'] = output;
132
+ if (Object.keys(compactResponse).length > 0)
133
+ out['response'] = compactResponse;
134
+ }
135
+ const item = isRecord(evt['item']) ? evt['item'] : undefined;
136
+ if (item && isSemanticToolType(item['type']))
137
+ out['item'] = item;
138
+ const delta = isRecord(evt['delta']) ? evt['delta'] : undefined;
139
+ if (delta) {
140
+ const compactDelta = {};
141
+ if (delta['type'] === 'input_json_delta')
142
+ compactDelta['type'] = delta['type'];
143
+ if (delta['partial_json'] !== undefined)
144
+ compactDelta['partial_json'] = delta['partial_json'];
145
+ if (Array.isArray(delta['tool_calls']))
146
+ compactDelta['tool_calls'] = delta['tool_calls'];
147
+ if (isRecord(delta['function_call']))
148
+ compactDelta['function_call'] = delta['function_call'];
149
+ if (Object.keys(compactDelta).length > 0)
150
+ out['delta'] = compactDelta;
151
+ }
152
+ const contentBlock = isRecord(evt['content_block']) ? evt['content_block'] : undefined;
153
+ if (contentBlock && isSemanticToolType(contentBlock['type']))
154
+ out['content_block'] = contentBlock;
155
+ return Object.keys(out).length > 0 ? out : null;
156
+ }
157
+ function streamEventHasSemanticValue(evt) {
158
+ if (!isRecord(evt))
159
+ return false;
160
+ const type = typeof evt['type'] === 'string' ? evt['type'] : '';
161
+ const message = isRecord(evt['message']) ? evt['message'] : undefined;
162
+ const response = isRecord(evt['response']) ? evt['response'] : undefined;
163
+ if (hasUsage(evt['usage']) || hasUsage(evt['usageMetadata']) || hasUsage(message?.['usage']) || hasUsage(response?.['usage']))
164
+ return true;
165
+ if (response?.['id'] || response?.['status'] || response?.['incomplete_details'] || message?.['id'] || evt['responseId'])
166
+ return true;
167
+ if (type === 'response.completed' || type === 'response.failed' || type === 'response.incomplete')
168
+ return true;
169
+ if (type.includes('function_call') || type.includes('tool'))
170
+ return true;
171
+ const item = isRecord(evt['item']) ? evt['item'] : undefined;
172
+ if (item && isSemanticToolType(item['type']))
173
+ return true;
174
+ const contentBlock = isRecord(evt['content_block']) ? evt['content_block'] : undefined;
175
+ if (contentBlock && isSemanticToolType(contentBlock['type']))
176
+ return true;
177
+ const delta = isRecord(evt['delta']) ? evt['delta'] : undefined;
178
+ if (delta && (delta['type'] === 'input_json_delta' || Array.isArray(delta['tool_calls']) || isRecord(delta['function_call'])))
179
+ return true;
180
+ const choices = Array.isArray(evt['choices']) ? evt['choices'] : [];
181
+ return choices.some((choice) => {
182
+ if (!isRecord(choice))
183
+ return false;
184
+ const choiceDelta = isRecord(choice['delta']) ? choice['delta'] : undefined;
185
+ const choiceMessage = isRecord(choice['message']) ? choice['message'] : undefined;
186
+ return choice['finish_reason'] !== undefined
187
+ || Array.isArray(choiceDelta?.['tool_calls'])
188
+ || isRecord(choiceDelta?.['function_call'])
189
+ || Array.isArray(choiceMessage?.['tool_calls'])
190
+ || isRecord(choiceMessage?.['function_call']);
191
+ });
192
+ }
193
+ function appendSemanticTailEvent(into, evt, limits) {
194
+ if (!streamEventHasSemanticValue(evt))
195
+ return;
196
+ const compact = compactStreamEvent(evt);
197
+ if (!compact)
198
+ return;
199
+ const tail = into.semantic_tail_events ?? [];
200
+ tail.push(compact);
201
+ if (tail.length > limits.semanticTailMaxEvents)
202
+ tail.splice(0, tail.length - limits.semanticTailMaxEvents);
203
+ into.semantic_tail_events = tail;
204
+ }
205
+ /** Extract a streamed text delta across the three wire formats: Anthropic content_block_delta(text_delta),
206
+ * OpenAI chat choices[].delta.content, OpenAI Responses response.output_text.delta. Best-effort; unknown shapes
207
+ * are ignored. */
208
+ function extractContentDelta(into, o, limits) {
209
+ const type = typeof o['type'] === 'string' ? o['type'] : '';
210
+ // Anthropic: { type: 'content_block_delta', delta: { type: 'text_delta', text: '...' } }
211
+ if (type === 'content_block_delta') {
212
+ const delta = o['delta'];
213
+ if (delta && typeof delta === 'object')
214
+ appendContent(into, delta['text'], limits);
215
+ return;
216
+ }
217
+ // OpenAI Responses: { type: 'response.output_text.delta', delta: '...' }
218
+ if (type === 'response.output_text.delta') {
219
+ appendContent(into, o['delta'], limits);
220
+ return;
221
+ }
222
+ // OpenAI Chat Completions: { choices: [{ delta: { content: '...' } }] }
223
+ const choices = o['choices'];
224
+ if (Array.isArray(choices) && choices[0] && typeof choices[0] === 'object') {
225
+ const delta = choices[0]['delta'];
226
+ if (delta && typeof delta === 'object')
227
+ appendContent(into, delta['content'], limits);
228
+ }
229
+ }
230
+ const USAGE_KEYS = ['input_tokens', 'output_tokens', 'cache_creation_input_tokens', 'cache_read_input_tokens'];
231
+ /** Partial-line buffer cap: a pathological stream with no newlines must not grow memory unbounded. OpenAI
232
+ * Responses `response.completed` can put the full response onto one data line, so keep this generous. */
233
+ export const MAX_PARTIAL_LINE_BYTES = DEFAULT_TRACE_ENVELOPE_MAX_CHARS;
234
+ const LARGE_LINE_SCAN_TAIL_BYTES = 128 * 1024;
235
+ function clipError(value) {
236
+ return value.replace(/\s+/g, ' ').trim().slice(0, 300);
237
+ }
238
+ function errMsg(err) {
239
+ return err instanceof Error ? err.message : String(err);
240
+ }
241
+ function mergeUsage(into, raw) {
242
+ if (!raw || typeof raw !== 'object')
243
+ return;
244
+ const u = raw;
245
+ const usage = into.usage ?? {};
246
+ for (const k of USAGE_KEYS)
247
+ if (typeof u[k] === 'number')
248
+ usage[k] = u[k];
249
+ if (typeof u['prompt_tokens'] === 'number')
250
+ usage.input_tokens = u['prompt_tokens'];
251
+ if (typeof u['completion_tokens'] === 'number')
252
+ usage.output_tokens = u['completion_tokens'];
253
+ const tokenDetails = u['input_tokens_details'] ?? u['prompt_tokens_details'];
254
+ if (tokenDetails && typeof tokenDetails === 'object') {
255
+ const cached = tokenDetails['cached_tokens'];
256
+ if (typeof cached === 'number')
257
+ usage.cache_read_input_tokens = cached;
258
+ }
259
+ if (Object.keys(usage).length > 0)
260
+ into.usage = usage;
261
+ }
262
+ function firstString(...values) {
263
+ for (const value of values) {
264
+ if (typeof value === 'string' && value.length > 0)
265
+ return value;
266
+ }
267
+ return null;
268
+ }
269
+ function nestedErrorMessage(value) {
270
+ if (!value || typeof value !== 'object')
271
+ return null;
272
+ const o = value;
273
+ return firstString(o['message'], o['type'], o['code']);
274
+ }
275
+ function responseIdFromEvent(o) {
276
+ const message = o['message'];
277
+ const response = o['response'];
278
+ return firstString(message && typeof message === 'object' ? message['id'] : undefined, response && typeof response === 'object' ? response['id'] : undefined, typeof o['id'] === 'string' && /^(?:resp_|chatcmpl-|msg_)/.test(o['id']) ? o['id'] : undefined);
279
+ }
280
+ function applyEvent(into, evt, captureContent = false, limits = sseCaptureLimits()) {
281
+ if (!evt || typeof evt !== 'object')
282
+ return;
283
+ const o = evt;
284
+ if (captureContent) {
285
+ appendEvent(into, evt, limits);
286
+ extractContentDelta(into, o, limits);
287
+ }
288
+ const message = o['message'];
289
+ const response = o['response'];
290
+ mergeUsage(into, o['usage']);
291
+ if (message && typeof message === 'object')
292
+ mergeUsage(into, message['usage']);
293
+ if (response && typeof response === 'object')
294
+ mergeUsage(into, response['usage']);
295
+ const delta = o['delta'];
296
+ if (delta && typeof delta === 'object') {
297
+ const sr = delta['stop_reason'];
298
+ if (typeof sr === 'string' || sr === null)
299
+ into.stop_reason = sr;
300
+ }
301
+ if (response && typeof response === 'object') {
302
+ const r = response;
303
+ const incomplete = r['incomplete_details'];
304
+ const reason = incomplete && typeof incomplete === 'object'
305
+ ? incomplete['reason']
306
+ : undefined;
307
+ if (typeof reason === 'string')
308
+ into.stop_reason = reason;
309
+ else if (typeof r['status'] === 'string')
310
+ into.stop_reason = r['status'];
311
+ if (r['status'] === 'failed' || r['status'] === 'cancelled') {
312
+ into.error = clipError(nestedErrorMessage(r['error']) ?? `provider stream ${r['status']}`);
313
+ }
314
+ }
315
+ const choices = o['choices'];
316
+ if (Array.isArray(choices) && choices[0] && typeof choices[0] === 'object') {
317
+ const finish = choices[0]['finish_reason'];
318
+ if (typeof finish === 'string' || finish === null)
319
+ into.stop_reason = finish;
320
+ }
321
+ const rid = responseIdFromEvent(o);
322
+ if (rid)
323
+ into.response_id = rid;
324
+ const type = typeof o['type'] === 'string' ? o['type'] : '';
325
+ const topError = nestedErrorMessage(o['error']);
326
+ if (type === 'error' || topError)
327
+ into.error = clipError(topError ?? 'provider stream error');
328
+ }
329
+ function setNumberUsage(into, key, match) {
330
+ if (!match?.[1])
331
+ return;
332
+ const n = Number(match[1]);
333
+ if (!Number.isFinite(n))
334
+ return;
335
+ const usage = into.usage ?? {};
336
+ usage[key] = n;
337
+ into.usage = usage;
338
+ }
339
+ function scanTextMetadata(into, text) {
340
+ const id = /"id"\s*:\s*"((?:resp_|chatcmpl-|msg_)[^"]+)"/.exec(text);
341
+ if (id?.[1])
342
+ into.response_id = id[1];
343
+ const status = /"status"\s*:\s*"(completed|failed|incomplete|cancelled)"/.exec(text);
344
+ if (status?.[1])
345
+ into.stop_reason = status[1];
346
+ setNumberUsage(into, 'input_tokens', /"input_tokens"\s*:\s*(\d+)/.exec(text));
347
+ setNumberUsage(into, 'output_tokens', /"output_tokens"\s*:\s*(\d+)/.exec(text));
348
+ setNumberUsage(into, 'input_tokens', /"prompt_tokens"\s*:\s*(\d+)/.exec(text));
349
+ setNumberUsage(into, 'output_tokens', /"completion_tokens"\s*:\s*(\d+)/.exec(text));
350
+ setNumberUsage(into, 'cache_read_input_tokens', /"cached_tokens"\s*:\s*(\d+)/.exec(text));
351
+ const looksLikeFailure = /"type"\s*:\s*"response\.failed"/.test(text)
352
+ || /"status"\s*:\s*"(failed|cancelled)"/.test(text);
353
+ if (!looksLikeFailure)
354
+ return;
355
+ const error = /"error"\s*:\s*\{[^{}]{0,2000}?"message"\s*:\s*"([^"]+)"/.exec(text)
356
+ ?? /"type"\s*:\s*"response\.failed"[^{}]{0,2000}?"message"\s*:\s*"([^"]+)"/.exec(text);
357
+ if (error?.[1])
358
+ into.error = clipError(error[1]);
359
+ }
360
+ export class SseUsageScanner {
361
+ result = {};
362
+ buf = '';
363
+ overCapLine = false;
364
+ largeLineTail = '';
365
+ rawStreamChars = 0;
366
+ decoder = new TextDecoder();
367
+ /** OPT-IN: when true, accumulate streamed completion text into result.content_text (bounded). Default false
368
+ * keeps the scanner metadata-only. Driven by the body-capture flag (see bodyCapture.ts). */
369
+ captureContent;
370
+ limits;
371
+ maxPartialLineBytes;
372
+ constructor(opts = {}) {
373
+ this.captureContent = opts.captureContent === true;
374
+ const env = opts.env ?? process.env;
375
+ this.limits = sseCaptureLimits(env);
376
+ this.maxPartialLineBytes = positiveIntegerFromEnv(env, ['EVOLVER_LLM_TRACE_STREAM_LINE_MAX_BYTES', 'EVOMAP_PROXY_TRACE_STREAM_LINE_MAX_BYTES'], MAX_PARTIAL_LINE_BYTES);
377
+ }
378
+ push(chunk) {
379
+ let text;
380
+ if (typeof chunk === 'string')
381
+ text = chunk;
382
+ else if (chunk instanceof Uint8Array)
383
+ text = this.decoder.decode(chunk, { stream: true });
384
+ else
385
+ return;
386
+ if (this.captureContent)
387
+ this.captureRawStream(text);
388
+ this.pushText(text, false);
389
+ }
390
+ finish() {
391
+ const finalText = this.decoder.decode();
392
+ if (finalText) {
393
+ if (this.captureContent)
394
+ this.captureRawStream(finalText);
395
+ this.pushText(finalText, false);
396
+ }
397
+ this.pushText('', true);
398
+ }
399
+ captureRawStream(text) {
400
+ if (!text)
401
+ return;
402
+ const redacted = redactText(text);
403
+ const current = this.result.raw_stream_body ?? '';
404
+ const available = Math.max(0, this.limits.rawStreamMaxChars - this.rawStreamChars);
405
+ if (available > 0) {
406
+ const piece = redacted.slice(0, available);
407
+ this.result.raw_stream_body = current + piece;
408
+ this.rawStreamChars += piece.length;
409
+ }
410
+ if (redacted.length > available)
411
+ this.result.raw_stream_truncated = true;
412
+ }
413
+ pushText(text, flush) {
414
+ if (this.overCapLine && text) {
415
+ this.scanLargeLineText(text);
416
+ }
417
+ this.buf += text;
418
+ for (;;) {
419
+ const nl = this.buf.indexOf('\n');
420
+ if (nl < 0)
421
+ break;
422
+ const line = this.buf.slice(0, nl).trim();
423
+ this.buf = this.buf.slice(nl + 1);
424
+ if (this.overCapLine) {
425
+ this.scanLargeLineText(line);
426
+ this.overCapLine = false;
427
+ this.largeLineTail = '';
428
+ continue;
429
+ }
430
+ this.scanLine(line);
431
+ }
432
+ if (flush && this.buf.length > 0) {
433
+ const line = this.buf.trim();
434
+ this.buf = '';
435
+ if (this.overCapLine) {
436
+ this.scanLargeLineText(line);
437
+ this.overCapLine = false;
438
+ this.largeLineTail = '';
439
+ const dataAt = line.indexOf('data:');
440
+ if (dataAt >= 0)
441
+ this.scanLine(line.slice(dataAt));
442
+ }
443
+ else {
444
+ this.scanLine(line);
445
+ }
446
+ }
447
+ if (this.buf.length > this.maxPartialLineBytes) {
448
+ this.markDroppedDataLine(this.buf);
449
+ this.scanLargeLineText(this.buf);
450
+ this.buf = '';
451
+ this.overCapLine = true;
452
+ }
453
+ }
454
+ scanLargeLineText(text) {
455
+ this.largeLineTail = (this.largeLineTail + text).slice(-LARGE_LINE_SCAN_TAIL_BYTES);
456
+ scanTextMetadata(this.result, text);
457
+ scanTextMetadata(this.result, this.largeLineTail);
458
+ }
459
+ markDroppedDataLine(line) {
460
+ if (!this.captureContent || !line.trimStart().startsWith('data:'))
461
+ return;
462
+ this.result.content_truncated = true;
463
+ this.result.dropped_event_count = (this.result.dropped_event_count ?? 0) + 1;
464
+ }
465
+ scanLine(line) {
466
+ if (!line.startsWith('data:'))
467
+ return;
468
+ const payload = line.slice(5).trim();
469
+ if (!payload || payload === '[DONE]')
470
+ return;
471
+ let evt;
472
+ try {
473
+ evt = JSON.parse(payload);
474
+ }
475
+ catch {
476
+ return;
477
+ }
478
+ applyEvent(this.result, evt, this.captureContent, this.limits);
479
+ }
480
+ }
481
+ /**
482
+ * Wrap a stream so every chunk passes to `push` before reaching the consumer, with `onEnd` fired exactly once
483
+ * on completion, error, or client cancel. Handles the two shapes _streamResponse relays (Web ReadableStream
484
+ * and async iterables); anything opaque is returned untouched with an immediate onEnd (trace without usage).
485
+ */
486
+ export function teeStreamForScan(stream, push, onEnd) {
487
+ let ended = false;
488
+ const finish = (info) => {
489
+ if (ended)
490
+ return;
491
+ ended = true;
492
+ try {
493
+ onEnd(info);
494
+ }
495
+ catch { /* trace emission must never break the relay */ }
496
+ };
497
+ if (stream && typeof stream.getReader === 'function') {
498
+ const reader = stream.getReader();
499
+ return new ReadableStream({
500
+ async pull(controller) {
501
+ try {
502
+ const { value, done } = await reader.read();
503
+ if (done) {
504
+ finish();
505
+ controller.close();
506
+ return;
507
+ }
508
+ push(value);
509
+ controller.enqueue(value);
510
+ }
511
+ catch (err) {
512
+ finish({ error: errMsg(err) });
513
+ controller.error(err);
514
+ }
515
+ },
516
+ cancel(reason) {
517
+ finish({ cancelled: true, ...(reason !== undefined ? { error: `stream cancelled: ${errMsg(reason)}` } : {}) });
518
+ return reader.cancel(reason).catch(() => undefined);
519
+ },
520
+ });
521
+ }
522
+ if (stream && typeof stream[Symbol.asyncIterator] === 'function') {
523
+ const src = stream;
524
+ return (async function* tee() {
525
+ let info;
526
+ try {
527
+ for await (const chunk of src) {
528
+ push(chunk);
529
+ yield chunk;
530
+ }
531
+ }
532
+ catch (err) {
533
+ info = { error: errMsg(err) };
534
+ throw err;
535
+ }
536
+ finally {
537
+ finish(info);
538
+ }
539
+ })();
540
+ }
541
+ finish();
542
+ return stream;
543
+ }