@dropout-ai/runtime 0.3.3 → 0.3.4
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/package.json +1 -1
- package/src/index.js +246 -302
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @dropout-ai/runtime
|
|
3
3
|
* Universal AI Interaction Capture for Node.js & Next.js
|
|
4
|
-
*
|
|
5
|
-
* Behavior: Fire-and-forget raw JSON to Supabase.
|
|
4
|
+
* Patches: fetch, http.request, https.request
|
|
6
5
|
* Capability: Captures Genkit, OpenAI, LangChain, Axios, and standard fetch.
|
|
7
6
|
*/
|
|
8
7
|
|
|
@@ -19,355 +18,300 @@ const KNOWN_AI_DOMAINS = [
|
|
|
19
18
|
'aiplatform.googleapis.com', 'api.groq.com', 'api.mistral.ai', 'api.cohere.ai'
|
|
20
19
|
];
|
|
21
20
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
privacyMode: (typeof process !== 'undefined' && process.env.DROPOUT_PRIVACY_MODE) || 'full',
|
|
30
|
-
debug: false // Added debug flag
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
let isPatched = false;
|
|
34
|
-
let turnIndex = 0;
|
|
35
|
-
let lastTurnConfirmed = true;
|
|
36
|
-
let lastPromptHash = null;
|
|
37
|
-
let lastResponseHash = null;
|
|
38
|
-
let instance = null;
|
|
39
|
-
|
|
40
|
-
// --- UTILS ---
|
|
41
|
-
|
|
42
|
-
function log(msg, ...args) {
|
|
43
|
-
if (activeConfig.debug) console.log(`[Dropout] ${msg}`, ...args);
|
|
44
|
-
}
|
|
21
|
+
class Dropout {
|
|
22
|
+
constructor(config = {}) {
|
|
23
|
+
// 1. Validation
|
|
24
|
+
if (!config.apiKey || !config.projectId) {
|
|
25
|
+
console.warn("[Dropout] ⚠️ Initialization Skipped: Missing apiKey or projectId.");
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
45
28
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
29
|
+
// 2. Config Setup
|
|
30
|
+
this.config = config;
|
|
31
|
+
this.projectId = config.projectId;
|
|
32
|
+
this.apiKey = config.apiKey;
|
|
33
|
+
this.debug = config.debug || false;
|
|
34
|
+
this.privacy = config.privacy || 'full';
|
|
35
|
+
this.captureEndpoint = SUPABASE_FUNCTION_URL;
|
|
36
|
+
|
|
37
|
+
// 3. Singleton Guard
|
|
38
|
+
if (global.__dropout_initialized__) {
|
|
39
|
+
if (this.debug) console.log("[Dropout] ℹ️ Already initialized. Skipping patch.");
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
global.__dropout_initialized__ = true;
|
|
53
43
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
} catch (e) { return 'hash_err'; }
|
|
59
|
-
}
|
|
44
|
+
// 4. Initialize Identity
|
|
45
|
+
if (!global.__dropout_session_id__) {
|
|
46
|
+
global.__dropout_session_id__ = this.generateSessionId();
|
|
47
|
+
}
|
|
60
48
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
let model = 'unknown';
|
|
64
|
-
|
|
65
|
-
if (url) {
|
|
66
|
-
const u = url.toLowerCase();
|
|
67
|
-
if (u.includes('openai')) provider = 'openai';
|
|
68
|
-
else if (u.includes('anthropic')) provider = 'anthropic';
|
|
69
|
-
else if (u.includes('google') || u.includes('generative') || u.includes('aiplatform')) provider = 'google';
|
|
70
|
-
else if (u.includes('groq')) provider = 'groq';
|
|
71
|
-
else if (u.includes('mistral')) provider = 'mistral';
|
|
72
|
-
else if (u.includes('cohere')) provider = 'cohere';
|
|
73
|
-
else if (u.includes('localhost') || u.includes('127.0.0.1')) provider = 'local';
|
|
74
|
-
}
|
|
49
|
+
// 5. Start the Wiretap
|
|
50
|
+
if (this.debug) console.log(`[Dropout] 🟢 Initialized for Project: ${this.projectId}`);
|
|
75
51
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
} catch (e) { }
|
|
52
|
+
// Bind methods to 'this' to avoid scope loss
|
|
53
|
+
this.log = this.log.bind(this);
|
|
54
|
+
this.isAiRequest = this.isAiRequest.bind(this);
|
|
55
|
+
this.patchNetwork();
|
|
81
56
|
}
|
|
82
|
-
return { provider, model };
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function isAiRequest(url, bodyString) {
|
|
86
|
-
if (!url) return false;
|
|
87
|
-
// Guard: Infinite Loop Prevention
|
|
88
|
-
if (url.includes(activeConfig.captureEndpoint)) return false;
|
|
89
57
|
|
|
90
|
-
//
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
// 2. Heuristic Check (for custom endpoints)
|
|
94
|
-
if (bodyString) {
|
|
95
|
-
return (bodyString.includes('"model"') || bodyString.includes('"messages"')) &&
|
|
96
|
-
(bodyString.includes('"user"') || bodyString.includes('"prompt"'));
|
|
58
|
+
// --- UTILS ---
|
|
59
|
+
log(msg, ...args) {
|
|
60
|
+
if (this.debug) console.log(`[Dropout] ${msg}`, ...args);
|
|
97
61
|
}
|
|
98
|
-
return false;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// --- EMITTER (Authenticated) ---
|
|
102
|
-
|
|
103
|
-
function emit(payload) {
|
|
104
|
-
// 1. Guard: Do not emit if not initialized
|
|
105
|
-
if (!activeConfig.apiKey || !activeConfig.projectId) return;
|
|
106
|
-
|
|
107
|
-
// 2. Construct Final Payload
|
|
108
|
-
const finalPayload = {
|
|
109
|
-
...payload,
|
|
110
|
-
project_id: activeConfig.projectId,
|
|
111
62
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
log(`🚀 Sending Capture (${payload.turn_role})`);
|
|
118
|
-
|
|
119
|
-
// 3. Fire and Forget using HTTPS (Bypassing our own patches)
|
|
120
|
-
const req = https.request(activeConfig.captureEndpoint, {
|
|
121
|
-
method: 'POST',
|
|
122
|
-
headers: {
|
|
123
|
-
'Content-Type': 'application/json',
|
|
124
|
-
'x-dropout-key': activeConfig.apiKey
|
|
63
|
+
generateSessionId() {
|
|
64
|
+
try {
|
|
65
|
+
return crypto.randomUUID();
|
|
66
|
+
} catch (e) {
|
|
67
|
+
return 'sess_' + Math.random().toString(36).substring(2, 12) + Date.now().toString(36);
|
|
125
68
|
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
req.on('error', (e) => log("❌ Upload Failed", e.message));
|
|
129
|
-
req.write(JSON.stringify(finalPayload));
|
|
130
|
-
req.end();
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// --- MONKEY PATCH ---
|
|
134
|
-
|
|
135
|
-
function applyPatch() {
|
|
136
|
-
if (isPatched) return;
|
|
137
|
-
|
|
138
|
-
// --- A. PATCH FETCH (Next.js / OpenAI) ---
|
|
139
|
-
if (GLOBAL_OBJ.fetch) {
|
|
140
|
-
GLOBAL_OBJ.__dropout_original_fetch__ = GLOBAL_OBJ.fetch;
|
|
141
|
-
GLOBAL_OBJ.fetch = async function (input, init) {
|
|
142
|
-
const url = typeof input === 'string' ? input : (input && input.url);
|
|
143
|
-
|
|
144
|
-
// Check body string safely
|
|
145
|
-
let bodyStr = "";
|
|
146
|
-
if (init && init.body) {
|
|
147
|
-
try { bodyStr = typeof init.body === 'string' ? init.body : JSON.stringify(init.body); } catch (e) { }
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// Guard
|
|
151
|
-
if (!isAiRequest(url, bodyStr)) {
|
|
152
|
-
return GLOBAL_OBJ.__dropout_original_fetch__(input, init);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
log(`⚡ [FETCH] Intercepting: ${url}`);
|
|
156
|
-
|
|
157
|
-
const start = Date.now();
|
|
69
|
+
}
|
|
158
70
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
activeTurn = turnIndex > 0 ? turnIndex - 1 : 0;
|
|
166
|
-
}
|
|
71
|
+
hash(text) {
|
|
72
|
+
if (!text) return null;
|
|
73
|
+
try {
|
|
74
|
+
return crypto.createHash('sha256').update(text.toLowerCase().trim()).digest('hex');
|
|
75
|
+
} catch (e) { return 'hash_err'; }
|
|
76
|
+
}
|
|
167
77
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
retry_like: pHash === lastPromptHash ? 1 : 0,
|
|
183
|
-
method: 'FETCH',
|
|
184
|
-
url: url
|
|
185
|
-
}
|
|
186
|
-
});
|
|
187
|
-
lastPromptHash = pHash;
|
|
78
|
+
normalize(url, body) {
|
|
79
|
+
let provider = 'custom';
|
|
80
|
+
let model = 'unknown';
|
|
81
|
+
|
|
82
|
+
if (url) {
|
|
83
|
+
const u = url.toLowerCase();
|
|
84
|
+
if (u.includes('openai')) provider = 'openai';
|
|
85
|
+
else if (u.includes('anthropic')) provider = 'anthropic';
|
|
86
|
+
else if (u.includes('google') || u.includes('gemini') || u.includes('aiplatform')) provider = 'google';
|
|
87
|
+
else if (u.includes('groq')) provider = 'groq';
|
|
88
|
+
else if (u.includes('mistral')) provider = 'mistral';
|
|
89
|
+
else if (u.includes('cohere')) provider = 'cohere';
|
|
90
|
+
else if (u.includes('localhost') || u.includes('127.0.0.1')) provider = 'local';
|
|
91
|
+
}
|
|
188
92
|
|
|
189
|
-
|
|
190
|
-
let response;
|
|
93
|
+
if (body) {
|
|
191
94
|
try {
|
|
192
|
-
|
|
193
|
-
|
|
95
|
+
const parsed = typeof body === 'string' ? JSON.parse(body) : body;
|
|
96
|
+
if (parsed.model) model = parsed.model;
|
|
97
|
+
} catch (e) { }
|
|
98
|
+
}
|
|
99
|
+
return { provider, model };
|
|
100
|
+
}
|
|
194
101
|
|
|
195
|
-
|
|
102
|
+
isAiRequest(url, bodyString) {
|
|
103
|
+
if (!url) return false;
|
|
104
|
+
// Guard: Infinite Loop Prevention
|
|
105
|
+
if (url.includes(this.captureEndpoint)) return false;
|
|
196
106
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
const cloned = response.clone();
|
|
200
|
-
const oText = await cloned.text();
|
|
201
|
-
const oHash = hash(oText);
|
|
202
|
-
|
|
203
|
-
emit({
|
|
204
|
-
session_id: GLOBAL_OBJ.__dropout_session_id__,
|
|
205
|
-
turn_index: activeTurn,
|
|
206
|
-
direction: 'ai_to_user',
|
|
207
|
-
turn_role: 'assistant',
|
|
208
|
-
latency_ms: latency,
|
|
209
|
-
provider,
|
|
210
|
-
model,
|
|
211
|
-
content: activeConfig.privacyMode === 'full' ? oText : null,
|
|
212
|
-
content_hash: oHash,
|
|
213
|
-
metadata_flags: {
|
|
214
|
-
non_adaptive_response: oHash === lastResponseHash ? 1 : 0,
|
|
215
|
-
turn_boundary_confirmed: 1,
|
|
216
|
-
status: response.status
|
|
217
|
-
}
|
|
218
|
-
});
|
|
219
|
-
lastResponseHash = oHash;
|
|
220
|
-
} catch (e) { log("⚠️ Failed to read response body"); }
|
|
107
|
+
// 1. Check Known Domains
|
|
108
|
+
if (KNOWN_AI_DOMAINS.some(d => url.includes(d))) return true;
|
|
221
109
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
110
|
+
// 2. Heuristic Check (for custom endpoints)
|
|
111
|
+
if (bodyString) {
|
|
112
|
+
return (bodyString.includes('"model"') || bodyString.includes('"messages"')) &&
|
|
113
|
+
(bodyString.includes('"user"') || bodyString.includes('"prompt"'));
|
|
114
|
+
}
|
|
115
|
+
return false;
|
|
226
116
|
}
|
|
227
117
|
|
|
228
|
-
// ---
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
const host = options.hostname || options.host || 'localhost';
|
|
242
|
-
const path = options.path || '/';
|
|
243
|
-
url = `${protocol}//${host}${path}`;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
// We can't check body here yet, so we rely on URL
|
|
247
|
-
if (!isAiRequest(url, null)) {
|
|
248
|
-
return originalRequest.apply(this, args);
|
|
249
|
-
}
|
|
118
|
+
// --- EMITTER ---
|
|
119
|
+
emit(payload) {
|
|
120
|
+
// 1. Guard
|
|
121
|
+
if (!this.apiKey || !this.projectId) return;
|
|
122
|
+
|
|
123
|
+
// 2. Construct Payload
|
|
124
|
+
const finalPayload = {
|
|
125
|
+
...payload,
|
|
126
|
+
project_id: this.projectId,
|
|
127
|
+
content_blob: payload.content, // Map for new schema
|
|
128
|
+
content: payload.content, // Map for old schema (fallback)
|
|
129
|
+
received_at: new Date().toISOString()
|
|
130
|
+
};
|
|
250
131
|
|
|
251
|
-
|
|
132
|
+
this.log(`🚀 Sending Capture (${payload.turn_role})`);
|
|
252
133
|
|
|
253
|
-
|
|
254
|
-
|
|
134
|
+
// 3. Fire and Forget using HTTPS (Bypassing our own patches via pure request)
|
|
135
|
+
// We use a clean request to avoid triggering our own hooks if we used global.fetch
|
|
136
|
+
const req = https.request(this.captureEndpoint, {
|
|
137
|
+
method: 'POST',
|
|
138
|
+
headers: {
|
|
139
|
+
'Content-Type': 'application/json',
|
|
140
|
+
'x-dropout-key': this.apiKey
|
|
141
|
+
}
|
|
142
|
+
});
|
|
255
143
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
144
|
+
req.on('error', (e) => this.log("❌ Upload Failed", e.message));
|
|
145
|
+
req.write(JSON.stringify(finalPayload));
|
|
146
|
+
req.end();
|
|
147
|
+
}
|
|
260
148
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
};
|
|
149
|
+
// --- CORE PATCHING ---
|
|
150
|
+
patchNetwork() {
|
|
151
|
+
const _this = this; // Capture instance
|
|
265
152
|
|
|
266
|
-
|
|
267
|
-
|
|
153
|
+
// --- A. PATCH FETCH ---
|
|
154
|
+
if (global.fetch) {
|
|
155
|
+
const originalFetch = global.fetch;
|
|
156
|
+
global.fetch = async function (input, init) {
|
|
157
|
+
const url = typeof input === 'string' ? input : input?.url;
|
|
268
158
|
|
|
269
|
-
//
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
159
|
+
// Safe Body Check
|
|
160
|
+
let bodyStr = "";
|
|
161
|
+
if (init && init.body) {
|
|
162
|
+
try { bodyStr = typeof init.body === 'string' ? init.body : JSON.stringify(init.body); } catch (e) { }
|
|
163
|
+
}
|
|
273
164
|
|
|
274
|
-
|
|
275
|
-
if (
|
|
276
|
-
|
|
277
|
-
lastTurnConfirmed = false;
|
|
278
|
-
} else {
|
|
279
|
-
activeTurn = turnIndex > 0 ? turnIndex - 1 : 0;
|
|
165
|
+
// Guard
|
|
166
|
+
if (!_this.isAiRequest(url, bodyStr)) {
|
|
167
|
+
return originalFetch.apply(this, arguments);
|
|
280
168
|
}
|
|
281
169
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
clientRequest._dropout_provider = provider;
|
|
285
|
-
clientRequest._dropout_model = model;
|
|
170
|
+
_this.log(`⚡ [FETCH] Intercepting: ${url}`);
|
|
171
|
+
const start = Date.now();
|
|
286
172
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
173
|
+
// Emit Request
|
|
174
|
+
const { provider, model } = _this.normalize(url, bodyStr);
|
|
175
|
+
_this.emit({
|
|
176
|
+
session_id: global.__dropout_session_id__,
|
|
177
|
+
turn_index: 0,
|
|
291
178
|
turn_role: 'user',
|
|
292
179
|
provider,
|
|
293
180
|
model,
|
|
294
|
-
content:
|
|
295
|
-
|
|
296
|
-
metadata_flags: { method: moduleName.toUpperCase(), url: url }
|
|
181
|
+
content: _this.privacy === 'full' ? bodyStr : null,
|
|
182
|
+
metadata_flags: { method: 'FETCH', url: url }
|
|
297
183
|
});
|
|
298
|
-
lastPromptHash = pHash;
|
|
299
184
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
const
|
|
311
|
-
|
|
312
|
-
emit({
|
|
313
|
-
session_id:
|
|
314
|
-
turn_index:
|
|
315
|
-
direction: 'ai_to_user',
|
|
185
|
+
// Execute
|
|
186
|
+
let response;
|
|
187
|
+
try {
|
|
188
|
+
response = await originalFetch.apply(this, arguments);
|
|
189
|
+
} catch (err) { throw err; }
|
|
190
|
+
|
|
191
|
+
const latency = Date.now() - start;
|
|
192
|
+
|
|
193
|
+
// Emit Response
|
|
194
|
+
try {
|
|
195
|
+
const cloned = response.clone();
|
|
196
|
+
const oText = await cloned.text();
|
|
197
|
+
_this.emit({
|
|
198
|
+
session_id: global.__dropout_session_id__,
|
|
199
|
+
turn_index: 0,
|
|
316
200
|
turn_role: 'assistant',
|
|
317
201
|
latency_ms: latency,
|
|
318
|
-
provider
|
|
319
|
-
model
|
|
320
|
-
content:
|
|
321
|
-
|
|
322
|
-
metadata_flags: { status: res.statusCode }
|
|
202
|
+
provider,
|
|
203
|
+
model,
|
|
204
|
+
content: _this.privacy === 'full' ? oText : null,
|
|
205
|
+
metadata_flags: { status: response.status }
|
|
323
206
|
});
|
|
207
|
+
} catch (e) { _this.log("⚠️ Failed to read response body"); }
|
|
324
208
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
209
|
+
return response;
|
|
210
|
+
};
|
|
211
|
+
this.log("✅ Patch Applied: global.fetch");
|
|
212
|
+
}
|
|
329
213
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
214
|
+
// --- B. PATCH NODE HTTP/HTTPS ---
|
|
215
|
+
const patchNodeRequest = (module, moduleName) => {
|
|
216
|
+
const originalRequest = module.request;
|
|
217
|
+
module.request = function (...args) {
|
|
218
|
+
let url;
|
|
219
|
+
// Argument Resolution (url, options, callback) or (options, callback)
|
|
220
|
+
if (typeof args[0] === 'string') {
|
|
221
|
+
url = args[0];
|
|
222
|
+
} else {
|
|
223
|
+
const opts = args[0] || {};
|
|
224
|
+
const protocol = opts.protocol || (moduleName === 'https' ? 'https:' : 'http:');
|
|
225
|
+
const host = opts.hostname || opts.host || 'localhost';
|
|
226
|
+
const path = opts.path || '/';
|
|
227
|
+
url = `${protocol}//${host}${path}`;
|
|
228
|
+
}
|
|
333
229
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
230
|
+
if (!_this.isAiRequest(url, null)) {
|
|
231
|
+
return originalRequest.apply(this, args);
|
|
232
|
+
}
|
|
337
233
|
|
|
338
|
-
|
|
339
|
-
|
|
234
|
+
_this.log(`⚡ [${moduleName.toUpperCase()}] Intercepting: ${url}`);
|
|
235
|
+
const start = Date.now();
|
|
340
236
|
|
|
341
|
-
|
|
237
|
+
const clientRequest = originalRequest.apply(this, args);
|
|
342
238
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
239
|
+
// Capture Buffers
|
|
240
|
+
const reqChunks = [];
|
|
241
|
+
const originalWrite = clientRequest.write;
|
|
242
|
+
const originalEnd = clientRequest.end;
|
|
346
243
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
244
|
+
// SAFE WRITE PATCH
|
|
245
|
+
clientRequest.write = function (...writeArgs) {
|
|
246
|
+
const chunk = writeArgs[0];
|
|
247
|
+
// Only buffer if it's data (String or Buffer), ignore objects/callbacks
|
|
248
|
+
if (chunk && (typeof chunk === 'string' || Buffer.isBuffer(chunk))) {
|
|
249
|
+
reqChunks.push(Buffer.from(chunk));
|
|
250
|
+
}
|
|
251
|
+
return originalWrite.apply(this, writeArgs);
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
// SAFE END PATCH
|
|
255
|
+
clientRequest.end = function (...endArgs) {
|
|
256
|
+
const chunk = endArgs[0];
|
|
257
|
+
// Only buffer if it's data
|
|
258
|
+
if (chunk && (typeof chunk === 'string' || Buffer.isBuffer(chunk))) {
|
|
259
|
+
reqChunks.push(Buffer.from(chunk));
|
|
260
|
+
}
|
|
351
261
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
262
|
+
// Emit Request
|
|
263
|
+
const reqBody = Buffer.concat(reqChunks).toString('utf8');
|
|
264
|
+
const { provider, model } = _this.normalize(url, reqBody);
|
|
265
|
+
|
|
266
|
+
// Attach state to request for response handler
|
|
267
|
+
clientRequest._dropout_meta = { provider, model };
|
|
268
|
+
|
|
269
|
+
_this.emit({
|
|
270
|
+
session_id: global.__dropout_session_id__,
|
|
271
|
+
turn_index: 0,
|
|
272
|
+
turn_role: 'user',
|
|
273
|
+
provider,
|
|
274
|
+
model,
|
|
275
|
+
content: _this.privacy === 'full' ? reqBody : null,
|
|
276
|
+
metadata_flags: { method: moduleName.toUpperCase(), url: url }
|
|
277
|
+
});
|
|
357
278
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
279
|
+
return originalEnd.apply(this, endArgs);
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
// Capture Response
|
|
283
|
+
clientRequest.on('response', (res) => {
|
|
284
|
+
const resChunks = [];
|
|
285
|
+
res.on('data', (chunk) => resChunks.push(chunk));
|
|
286
|
+
res.on('end', () => {
|
|
287
|
+
const resBody = Buffer.concat(resChunks).toString('utf8');
|
|
288
|
+
const latency = Date.now() - start;
|
|
289
|
+
const meta = clientRequest._dropout_meta || {};
|
|
290
|
+
|
|
291
|
+
_this.emit({
|
|
292
|
+
session_id: global.__dropout_session_id__,
|
|
293
|
+
turn_index: 0,
|
|
294
|
+
turn_role: 'assistant',
|
|
295
|
+
latency_ms: latency,
|
|
296
|
+
provider: meta.provider,
|
|
297
|
+
model: meta.model,
|
|
298
|
+
content: _this.privacy === 'full' ? resBody : null,
|
|
299
|
+
metadata_flags: { status: res.statusCode }
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
return clientRequest;
|
|
305
|
+
};
|
|
306
|
+
};
|
|
362
307
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
instance = this;
|
|
308
|
+
patchNodeRequest(https, 'https');
|
|
309
|
+
patchNodeRequest(http, 'http');
|
|
310
|
+
this.log("✅ Patch Applied: http/https");
|
|
367
311
|
}
|
|
368
312
|
}
|
|
369
313
|
|
|
370
|
-
// Auto-Start
|
|
314
|
+
// Auto-Start (Node Preload)
|
|
371
315
|
if (process.env.DROPOUT_PROJECT_ID && process.env.DROPOUT_API_KEY) {
|
|
372
316
|
new Dropout({
|
|
373
317
|
projectId: process.env.DROPOUT_PROJECT_ID,
|