@autotask/atools-tool 0.1.7 → 0.1.8
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/lib/proxy-server.mjs +453 -8
- package/package.json +1 -1
package/lib/proxy-server.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import http from 'node:http';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import crypto from 'node:crypto';
|
|
4
5
|
|
|
5
6
|
const DEFAULT_PORT = 18888;
|
|
6
7
|
const DEFAULT_UPSTREAM = 'https://sub2api.atools.live';
|
|
@@ -10,6 +11,9 @@ const DEFAULT_MAX_REQ_BYTES = 68000;
|
|
|
10
11
|
const DEFAULT_DROP_TOOLS_ON_COMPACT = false;
|
|
11
12
|
const DEFAULT_STRIP_PREVIOUS_RESPONSE_ID = false;
|
|
12
13
|
const DEFAULT_FORCE_MODEL = '';
|
|
14
|
+
const DEFAULT_DEBUG_DUMP = false;
|
|
15
|
+
const DEFAULT_DEBUG_MAX_BODY = 800000;
|
|
16
|
+
const DEFAULT_DEBUG_INCLUDE_AUTH = false;
|
|
13
17
|
const SUPPORTED_MODEL_IDS = new Set(['gpt-5.2', 'gpt-5.3-codex', 'gpt-5.4']);
|
|
14
18
|
const FORWARD_HEADER_ALLOWLIST = new Set([
|
|
15
19
|
'authorization',
|
|
@@ -111,6 +115,15 @@ function normalizeResponsesPayload(payload, {
|
|
|
111
115
|
});
|
|
112
116
|
}
|
|
113
117
|
|
|
118
|
+
const hasFunctionCallOutput = extractFunctionCallOutputIds(out).length > 0;
|
|
119
|
+
if ((Array.isArray(out.tools) && out.tools.length > 0) || hasFunctionCallOutput) {
|
|
120
|
+
out.store = true;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (Array.isArray(out.tools) && out.tools.length > 0 && (out.tool_choice == null || out.tool_choice === '')) {
|
|
124
|
+
out.tool_choice = 'auto';
|
|
125
|
+
}
|
|
126
|
+
|
|
114
127
|
return pruneResponsesPayload(out);
|
|
115
128
|
}
|
|
116
129
|
|
|
@@ -200,6 +213,236 @@ function sleep(ms) {
|
|
|
200
213
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
201
214
|
}
|
|
202
215
|
|
|
216
|
+
function parseBool(value, fallback = false) {
|
|
217
|
+
if (typeof value === 'boolean') return value;
|
|
218
|
+
if (value == null) return fallback;
|
|
219
|
+
const normalized = String(value).trim().toLowerCase();
|
|
220
|
+
if (!normalized) return fallback;
|
|
221
|
+
return ['1', 'true', 'yes', 'on'].includes(normalized);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function maskSecret(value) {
|
|
225
|
+
const text = String(value ?? '');
|
|
226
|
+
if (text.length <= 10) return '***';
|
|
227
|
+
return `${text.slice(0, 6)}...${text.slice(-4)}`;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function toHeaderObject(headers, { includeAuth = DEFAULT_DEBUG_INCLUDE_AUTH } = {}) {
|
|
231
|
+
const out = {};
|
|
232
|
+
if (!headers) return out;
|
|
233
|
+
|
|
234
|
+
if (typeof headers.forEach === 'function') {
|
|
235
|
+
headers.forEach((value, key) => {
|
|
236
|
+
const low = String(key || '').toLowerCase();
|
|
237
|
+
if (!includeAuth && (low === 'authorization' || low === 'api-key' || low === 'x-api-key')) {
|
|
238
|
+
out[key] = maskSecret(value);
|
|
239
|
+
} else {
|
|
240
|
+
out[key] = String(value);
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
return out;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
for (const [key, rawValue] of Object.entries(headers)) {
|
|
247
|
+
const value = normalizeHeaderValue(rawValue);
|
|
248
|
+
const low = String(key || '').toLowerCase();
|
|
249
|
+
if (!includeAuth && (low === 'authorization' || low === 'api-key' || low === 'x-api-key')) {
|
|
250
|
+
out[key] = maskSecret(value);
|
|
251
|
+
} else {
|
|
252
|
+
out[key] = value;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return out;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function bufferToDebugText(buf, maxChars = DEFAULT_DEBUG_MAX_BODY) {
|
|
259
|
+
const text = Buffer.isBuffer(buf) ? buf.toString('utf8') : String(buf ?? '');
|
|
260
|
+
if (!Number.isFinite(maxChars) || maxChars <= 0) return text;
|
|
261
|
+
if (text.length <= maxChars) return text;
|
|
262
|
+
return `${text.slice(0, maxChars)}\n...[truncated ${text.length - maxChars} chars]`;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function hashKey(text) {
|
|
266
|
+
return crypto.createHash('sha1').update(String(text)).digest('hex').slice(0, 16);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function extractAuthIdentity(headers = {}) {
|
|
270
|
+
const auth = normalizeHeaderValue(headers.authorization || headers.Authorization || '');
|
|
271
|
+
if (auth) return auth;
|
|
272
|
+
const apiKey = normalizeHeaderValue(headers['x-api-key'] || headers['api-key'] || '');
|
|
273
|
+
return apiKey;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function buildConversationKey(headers = {}, payload = {}) {
|
|
277
|
+
const authIdentity = extractAuthIdentity(headers);
|
|
278
|
+
if (!authIdentity) return '';
|
|
279
|
+
const modelRaw = typeof payload?.model === 'string' ? payload.model : '*';
|
|
280
|
+
const model = normalizeModelId(modelRaw) || '*';
|
|
281
|
+
return hashKey(`${authIdentity}|${model}`);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function buildPromptContextKey(headers = {}, payload = {}) {
|
|
285
|
+
const authIdentity = extractAuthIdentity(headers);
|
|
286
|
+
if (!authIdentity) return '';
|
|
287
|
+
const promptCacheKey = typeof payload?.prompt_cache_key === 'string'
|
|
288
|
+
? payload.prompt_cache_key.trim()
|
|
289
|
+
: '';
|
|
290
|
+
if (!promptCacheKey) return '';
|
|
291
|
+
const modelRaw = typeof payload?.model === 'string' ? payload.model : '*';
|
|
292
|
+
const model = normalizeModelId(modelRaw) || '*';
|
|
293
|
+
return hashKey(`${authIdentity}|${model}|${promptCacheKey}`);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function extractFunctionCallOutputIds(payload = {}) {
|
|
297
|
+
const ids = [];
|
|
298
|
+
const input = Array.isArray(payload.input) ? payload.input : [];
|
|
299
|
+
for (const item of input) {
|
|
300
|
+
if (!item || typeof item !== 'object') continue;
|
|
301
|
+
|
|
302
|
+
if (item.type === 'function_call_output' && typeof item.call_id === 'string' && item.call_id) {
|
|
303
|
+
ids.push(item.call_id);
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (Array.isArray(item.content)) {
|
|
308
|
+
for (const part of item.content) {
|
|
309
|
+
if (!part || typeof part !== 'object') continue;
|
|
310
|
+
if (part.type === 'function_call_output' && typeof part.call_id === 'string' && part.call_id) {
|
|
311
|
+
ids.push(part.call_id);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return ids;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function extractItemReferenceIds(payload = {}) {
|
|
320
|
+
const ids = [];
|
|
321
|
+
const input = Array.isArray(payload.input) ? payload.input : [];
|
|
322
|
+
for (const item of input) {
|
|
323
|
+
if (!item || typeof item !== 'object') continue;
|
|
324
|
+
if (item.type === 'item_reference' && typeof item.id === 'string' && item.id) {
|
|
325
|
+
ids.push(item.id);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return ids;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function extractResponseId(contentType, buffer) {
|
|
332
|
+
const body = bufferToDebugText(buffer, 3_000_000);
|
|
333
|
+
const ct = String(contentType || '').toLowerCase();
|
|
334
|
+
|
|
335
|
+
if (ct.includes('application/json')) {
|
|
336
|
+
try {
|
|
337
|
+
const parsed = JSON.parse(body);
|
|
338
|
+
if (parsed && typeof parsed.id === 'string' && parsed.id) return parsed.id;
|
|
339
|
+
if (parsed?.response && typeof parsed.response.id === 'string' && parsed.response.id) return parsed.response.id;
|
|
340
|
+
} catch {
|
|
341
|
+
return '';
|
|
342
|
+
}
|
|
343
|
+
return '';
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (ct.includes('text/event-stream')) {
|
|
347
|
+
const chunks = body.split('\n\n');
|
|
348
|
+
for (const chunk of chunks) {
|
|
349
|
+
const dataLines = chunk.split('\n').filter((line) => line.startsWith('data: '));
|
|
350
|
+
if (!dataLines.length) continue;
|
|
351
|
+
const dataText = dataLines.map((line) => line.slice(6)).join('\n');
|
|
352
|
+
if (!dataText || dataText === '[DONE]') continue;
|
|
353
|
+
try {
|
|
354
|
+
const parsed = JSON.parse(dataText);
|
|
355
|
+
if (parsed?.type === 'response.created' && typeof parsed?.response?.id === 'string') {
|
|
356
|
+
return parsed.response.id;
|
|
357
|
+
}
|
|
358
|
+
if (parsed?.type === 'response.completed' && typeof parsed?.response?.id === 'string') {
|
|
359
|
+
return parsed.response.id;
|
|
360
|
+
}
|
|
361
|
+
} catch {
|
|
362
|
+
// ignore malformed SSE chunk
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return '';
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function setCachedResponseId(cache, key, responseId) {
|
|
371
|
+
if (!key || !responseId) return;
|
|
372
|
+
cache.set(key, responseId);
|
|
373
|
+
if (cache.size > 300) {
|
|
374
|
+
const first = cache.keys().next();
|
|
375
|
+
if (!first.done) cache.delete(first.value);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function setCachedCallIdContext(cache, callId, responseId) {
|
|
380
|
+
if (!callId || !responseId) return;
|
|
381
|
+
cache.set(callId, responseId);
|
|
382
|
+
if (cache.size > 1200) {
|
|
383
|
+
const first = cache.keys().next();
|
|
384
|
+
if (!first.done) cache.delete(first.value);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function extractFunctionCallIdsFromResponse(contentType, buffer) {
|
|
389
|
+
const ids = [];
|
|
390
|
+
const body = bufferToDebugText(buffer, 3_000_000);
|
|
391
|
+
const ct = String(contentType || '').toLowerCase();
|
|
392
|
+
|
|
393
|
+
if (ct.includes('application/json')) {
|
|
394
|
+
try {
|
|
395
|
+
const parsed = JSON.parse(body);
|
|
396
|
+
const output = Array.isArray(parsed?.output) ? parsed.output : [];
|
|
397
|
+
for (const item of output) {
|
|
398
|
+
if (item?.type === 'function_call' && typeof item.call_id === 'string' && item.call_id) {
|
|
399
|
+
ids.push(item.call_id);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
} catch {
|
|
403
|
+
return ids;
|
|
404
|
+
}
|
|
405
|
+
return ids;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (ct.includes('text/event-stream')) {
|
|
409
|
+
const chunks = body.split('\n\n');
|
|
410
|
+
for (const chunk of chunks) {
|
|
411
|
+
const dataLines = chunk.split('\n').filter((line) => line.startsWith('data: '));
|
|
412
|
+
if (!dataLines.length) continue;
|
|
413
|
+
const dataText = dataLines.map((line) => line.slice(6)).join('\n');
|
|
414
|
+
if (!dataText || dataText === '[DONE]') continue;
|
|
415
|
+
try {
|
|
416
|
+
const parsed = JSON.parse(dataText);
|
|
417
|
+
if (parsed?.type === 'response.output_item.added') {
|
|
418
|
+
const item = parsed?.item;
|
|
419
|
+
if (item?.type === 'function_call' && typeof item.call_id === 'string' && item.call_id) {
|
|
420
|
+
ids.push(item.call_id);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
} catch {
|
|
424
|
+
// ignore malformed SSE chunk
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
return ids;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function parseSseEventDataText(eventBlock = '') {
|
|
433
|
+
const dataLines = String(eventBlock)
|
|
434
|
+
.split('\n')
|
|
435
|
+
.filter((line) => line.startsWith('data: '));
|
|
436
|
+
if (!dataLines.length) return null;
|
|
437
|
+
const dataText = dataLines.map((line) => line.slice(6)).join('\n');
|
|
438
|
+
if (!dataText || dataText === '[DONE]') return null;
|
|
439
|
+
try {
|
|
440
|
+
return JSON.parse(dataText);
|
|
441
|
+
} catch {
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
203
446
|
async function forward(url, req, headers, body) {
|
|
204
447
|
return fetch(url, {
|
|
205
448
|
method: req.method,
|
|
@@ -252,6 +495,15 @@ export async function createProxyServer(options = {}) {
|
|
|
252
495
|
const stripPreviousResponseId = options.stripPreviousResponseId
|
|
253
496
|
?? ['1', 'true', 'yes', 'on'].includes(String(process.env.SUB2API_COMPAT_STRIP_PREVIOUS_RESPONSE_ID || '').toLowerCase());
|
|
254
497
|
const forceModel = String(options.forceModel ?? process.env.SUB2API_COMPAT_FORCE_MODEL ?? '').trim();
|
|
498
|
+
const debugDump = parseBool(options.debugDump ?? process.env.SUB2API_COMPAT_DEBUG_DUMP, DEFAULT_DEBUG_DUMP);
|
|
499
|
+
const debugMaxBody = Number(options.debugMaxBody ?? process.env.SUB2API_COMPAT_DEBUG_MAX_BODY ?? DEFAULT_DEBUG_MAX_BODY);
|
|
500
|
+
const debugIncludeAuth = parseBool(
|
|
501
|
+
options.debugIncludeAuth ?? process.env.SUB2API_COMPAT_DEBUG_INCLUDE_AUTH,
|
|
502
|
+
DEFAULT_DEBUG_INCLUDE_AUTH
|
|
503
|
+
);
|
|
504
|
+
const responseContextCache = new Map();
|
|
505
|
+
const promptContextCache = new Map();
|
|
506
|
+
const callIdContextCache = new Map();
|
|
255
507
|
|
|
256
508
|
const server = http.createServer(async (req, res) => {
|
|
257
509
|
const startAt = Date.now();
|
|
@@ -262,21 +514,83 @@ export async function createProxyServer(options = {}) {
|
|
|
262
514
|
const rawBody = Buffer.concat(chunks);
|
|
263
515
|
|
|
264
516
|
const baseHeaders = buildCodexLikeHeaders(req.headers, userAgent);
|
|
517
|
+
const incomingHeaders = toHeaderObject(req.headers, { includeAuth: debugIncludeAuth });
|
|
265
518
|
|
|
266
519
|
const isResponsesPath = req.method === 'POST' && url.pathname.endsWith('/responses');
|
|
267
520
|
let body = rawBody;
|
|
268
521
|
let compactBody = null;
|
|
269
522
|
let aggressiveBody = null;
|
|
270
523
|
let minimalBody = null;
|
|
524
|
+
let requestedStream = false;
|
|
525
|
+
let rawBodyText = '';
|
|
526
|
+
let normalizedBodyText = '';
|
|
527
|
+
let conversationKey = '';
|
|
528
|
+
let promptContextKey = '';
|
|
529
|
+
let functionCallOutputIds = [];
|
|
271
530
|
|
|
272
531
|
if (isResponsesPath && rawBody.length > 0) {
|
|
273
532
|
try {
|
|
274
533
|
const parsed = JSON.parse(rawBody.toString('utf8'));
|
|
534
|
+
rawBodyText = JSON.stringify(parsed);
|
|
275
535
|
const inputModel = typeof parsed.model === 'string' ? parsed.model : '';
|
|
276
536
|
const normalized = normalizeResponsesPayload(parsed, {
|
|
277
537
|
stripPreviousResponseId,
|
|
278
538
|
forceModel
|
|
279
539
|
});
|
|
540
|
+
requestedStream = normalized?.stream === true;
|
|
541
|
+
conversationKey = buildConversationKey(req.headers, normalized);
|
|
542
|
+
promptContextKey = buildPromptContextKey(req.headers, normalized);
|
|
543
|
+
functionCallOutputIds = extractFunctionCallOutputIds(normalized);
|
|
544
|
+
if (functionCallOutputIds.length > 0 && !normalized.previous_response_id) {
|
|
545
|
+
let cachedResponseId = '';
|
|
546
|
+
let source = '';
|
|
547
|
+
const matched = new Set();
|
|
548
|
+
for (const callId of functionCallOutputIds) {
|
|
549
|
+
const responseId = callIdContextCache.get(callId);
|
|
550
|
+
if (responseId) matched.add(responseId);
|
|
551
|
+
}
|
|
552
|
+
if (matched.size === 1) {
|
|
553
|
+
cachedResponseId = [...matched][0];
|
|
554
|
+
source = 'call_id';
|
|
555
|
+
} else if (promptContextKey) {
|
|
556
|
+
cachedResponseId = promptContextCache.get(promptContextKey) || '';
|
|
557
|
+
if (cachedResponseId) source = 'prompt_cache_key';
|
|
558
|
+
}
|
|
559
|
+
if (!cachedResponseId && conversationKey) {
|
|
560
|
+
cachedResponseId = responseContextCache.get(conversationKey) || '';
|
|
561
|
+
if (cachedResponseId) source = 'conversation';
|
|
562
|
+
}
|
|
563
|
+
if (cachedResponseId) {
|
|
564
|
+
normalized.previous_response_id = cachedResponseId;
|
|
565
|
+
normalized.store = true;
|
|
566
|
+
appendLog(logFile, {
|
|
567
|
+
method: req.method,
|
|
568
|
+
path: url.pathname,
|
|
569
|
+
contextRepair: {
|
|
570
|
+
source,
|
|
571
|
+
previous_response_id: cachedResponseId,
|
|
572
|
+
function_call_output_count: functionCallOutputIds.length
|
|
573
|
+
}
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
if (!cachedResponseId && Array.isArray(normalized.input)) {
|
|
577
|
+
const existingRefs = new Set(extractItemReferenceIds(normalized));
|
|
578
|
+
const missingCallIds = [...new Set(functionCallOutputIds)].filter((id) => !existingRefs.has(id));
|
|
579
|
+
if (missingCallIds.length > 0) {
|
|
580
|
+
const refs = missingCallIds.map((id) => ({ type: 'item_reference', id }));
|
|
581
|
+
normalized.input = [...refs, ...normalized.input];
|
|
582
|
+
appendLog(logFile, {
|
|
583
|
+
method: req.method,
|
|
584
|
+
path: url.pathname,
|
|
585
|
+
contextRepair: {
|
|
586
|
+
item_reference_injected: missingCallIds.length
|
|
587
|
+
}
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
requestedStream = normalized?.stream === true;
|
|
593
|
+
normalizedBodyText = JSON.stringify(normalized);
|
|
280
594
|
const outputModel = typeof normalized.model === 'string' ? normalized.model : '';
|
|
281
595
|
body = Buffer.from(JSON.stringify(normalized));
|
|
282
596
|
if (body.length > maxReqBytes) {
|
|
@@ -310,8 +624,11 @@ export async function createProxyServer(options = {}) {
|
|
|
310
624
|
});
|
|
311
625
|
}
|
|
312
626
|
} catch {
|
|
627
|
+
rawBodyText = bufferToDebugText(rawBody, debugMaxBody);
|
|
313
628
|
// Keep original body if parse fails.
|
|
314
629
|
}
|
|
630
|
+
} else if (rawBody.length > 0) {
|
|
631
|
+
rawBodyText = bufferToDebugText(rawBody, debugMaxBody);
|
|
315
632
|
}
|
|
316
633
|
|
|
317
634
|
const requestBodies = [body];
|
|
@@ -330,11 +647,17 @@ export async function createProxyServer(options = {}) {
|
|
|
330
647
|
requestBodies.push(minimalBody);
|
|
331
648
|
}
|
|
332
649
|
|
|
333
|
-
let resp;
|
|
334
650
|
let attempts = 0;
|
|
335
651
|
let primaryStatus = null;
|
|
336
652
|
let compacted = false;
|
|
337
653
|
let strategy = 'full';
|
|
654
|
+
let finalStatus = 502;
|
|
655
|
+
let finalHeaders = {};
|
|
656
|
+
let outBuffer = Buffer.alloc(0);
|
|
657
|
+
let streamResp = null;
|
|
658
|
+
let streamObservedResponseId = '';
|
|
659
|
+
const streamObservedCallIds = new Set();
|
|
660
|
+
const debugAttempts = [];
|
|
338
661
|
const maxAttempts = isResponsesPath ? retryMax : 1;
|
|
339
662
|
while (attempts < maxAttempts) {
|
|
340
663
|
attempts += 1;
|
|
@@ -347,8 +670,42 @@ export async function createProxyServer(options = {}) {
|
|
|
347
670
|
const primaryHeaders = isResponsesPath ? buildHeaders(baseHeaders, requestBody.length) : baseHeaders;
|
|
348
671
|
|
|
349
672
|
const primaryResp = await forward(url, req, primaryHeaders, requestBody);
|
|
673
|
+
const attemptHeaders = toHeaderObject(primaryResp.headers, { includeAuth: debugIncludeAuth });
|
|
350
674
|
primaryStatus = primaryResp.status;
|
|
351
|
-
|
|
675
|
+
finalStatus = primaryResp.status;
|
|
676
|
+
finalHeaders = attemptHeaders;
|
|
677
|
+
|
|
678
|
+
if (requestedStream && primaryResp.status < 500) {
|
|
679
|
+
streamResp = primaryResp;
|
|
680
|
+
if (debugDump) {
|
|
681
|
+
debugAttempts.push({
|
|
682
|
+
attempt: attempts,
|
|
683
|
+
strategy,
|
|
684
|
+
requestHeaders: toHeaderObject(primaryHeaders, { includeAuth: debugIncludeAuth }),
|
|
685
|
+
requestBody: bufferToDebugText(requestBody, debugMaxBody),
|
|
686
|
+
responseStatus: primaryResp.status,
|
|
687
|
+
responseHeaders: attemptHeaders,
|
|
688
|
+
responseBody: '[stream passthrough]'
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
break;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
const attemptBuffer = Buffer.from(await primaryResp.arrayBuffer());
|
|
695
|
+
outBuffer = attemptBuffer;
|
|
696
|
+
|
|
697
|
+
if (debugDump) {
|
|
698
|
+
debugAttempts.push({
|
|
699
|
+
attempt: attempts,
|
|
700
|
+
strategy,
|
|
701
|
+
requestHeaders: toHeaderObject(primaryHeaders, { includeAuth: debugIncludeAuth }),
|
|
702
|
+
requestBody: bufferToDebugText(requestBody, debugMaxBody),
|
|
703
|
+
responseStatus: primaryResp.status,
|
|
704
|
+
responseHeaders: attemptHeaders,
|
|
705
|
+
responseBody: bufferToDebugText(attemptBuffer, debugMaxBody)
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
|
|
352
709
|
if (primaryResp.status < 500) break;
|
|
353
710
|
|
|
354
711
|
if (attempts < maxAttempts) {
|
|
@@ -356,20 +713,91 @@ export async function createProxyServer(options = {}) {
|
|
|
356
713
|
}
|
|
357
714
|
}
|
|
358
715
|
|
|
359
|
-
res.statusCode =
|
|
360
|
-
|
|
716
|
+
res.statusCode = finalStatus;
|
|
717
|
+
Object.entries(finalHeaders).forEach(([key, value]) => {
|
|
361
718
|
if (key.toLowerCase() === 'transfer-encoding') return;
|
|
362
719
|
res.setHeader(key, value);
|
|
363
720
|
});
|
|
364
721
|
|
|
365
|
-
|
|
366
|
-
|
|
722
|
+
if (streamResp?.body) {
|
|
723
|
+
const streamed = [];
|
|
724
|
+
let sseCarry = '';
|
|
725
|
+
for await (const chunk of streamResp.body) {
|
|
726
|
+
const buf = Buffer.from(chunk);
|
|
727
|
+
streamed.push(buf);
|
|
728
|
+
if (isResponsesPath) {
|
|
729
|
+
sseCarry += buf.toString('utf8').replace(/\r\n/g, '\n');
|
|
730
|
+
let splitIndex = sseCarry.indexOf('\n\n');
|
|
731
|
+
while (splitIndex >= 0) {
|
|
732
|
+
const eventBlock = sseCarry.slice(0, splitIndex);
|
|
733
|
+
sseCarry = sseCarry.slice(splitIndex + 2);
|
|
734
|
+
const parsedEvent = parseSseEventDataText(eventBlock);
|
|
735
|
+
if (parsedEvent) {
|
|
736
|
+
const eventResponseId = typeof parsedEvent?.response?.id === 'string'
|
|
737
|
+
? parsedEvent.response.id
|
|
738
|
+
: '';
|
|
739
|
+
if (!streamObservedResponseId && eventResponseId) {
|
|
740
|
+
streamObservedResponseId = eventResponseId;
|
|
741
|
+
setCachedResponseId(responseContextCache, conversationKey, streamObservedResponseId);
|
|
742
|
+
setCachedResponseId(promptContextCache, promptContextKey, streamObservedResponseId);
|
|
743
|
+
}
|
|
744
|
+
if (
|
|
745
|
+
parsedEvent?.type === 'response.output_item.added'
|
|
746
|
+
|| parsedEvent?.type === 'response.output_item.done'
|
|
747
|
+
) {
|
|
748
|
+
const item = parsedEvent?.item;
|
|
749
|
+
const callId = (item?.type === 'function_call' && typeof item?.call_id === 'string')
|
|
750
|
+
? item.call_id
|
|
751
|
+
: '';
|
|
752
|
+
if (callId) {
|
|
753
|
+
streamObservedCallIds.add(callId);
|
|
754
|
+
if (streamObservedResponseId) {
|
|
755
|
+
setCachedCallIdContext(callIdContextCache, callId, streamObservedResponseId);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
splitIndex = sseCarry.indexOf('\n\n');
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
res.write(buf);
|
|
764
|
+
}
|
|
765
|
+
outBuffer = Buffer.concat(streamed);
|
|
766
|
+
res.end();
|
|
767
|
+
} else {
|
|
768
|
+
res.end(outBuffer);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
const upstreamError = finalStatus >= 500
|
|
367
772
|
? outBuffer.toString('utf8').replace(/\s+/g, ' ').slice(0, 240)
|
|
368
773
|
: '';
|
|
774
|
+
const finalContentType = finalHeaders['content-type'] || '';
|
|
775
|
+
const responseId = streamObservedResponseId || extractResponseId(finalContentType, outBuffer);
|
|
776
|
+
if (responseId) {
|
|
777
|
+
setCachedResponseId(responseContextCache, conversationKey, responseId);
|
|
778
|
+
setCachedResponseId(promptContextCache, promptContextKey, responseId);
|
|
779
|
+
const callIds = extractFunctionCallIdsFromResponse(finalContentType, outBuffer);
|
|
780
|
+
for (const callId of streamObservedCallIds) {
|
|
781
|
+
setCachedCallIdContext(callIdContextCache, callId, responseId);
|
|
782
|
+
}
|
|
783
|
+
for (const callId of callIds) {
|
|
784
|
+
setCachedCallIdContext(callIdContextCache, callId, responseId);
|
|
785
|
+
}
|
|
786
|
+
if (callIds.length > 0 || streamObservedCallIds.size > 0) {
|
|
787
|
+
appendLog(logFile, {
|
|
788
|
+
method: req.method,
|
|
789
|
+
path: url.pathname,
|
|
790
|
+
contextCacheUpdate: {
|
|
791
|
+
response_id: responseId,
|
|
792
|
+
call_ids: Math.max(callIds.length, streamObservedCallIds.size)
|
|
793
|
+
}
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
}
|
|
369
797
|
appendLog(logFile, {
|
|
370
798
|
method: req.method,
|
|
371
799
|
path: url.pathname,
|
|
372
|
-
status:
|
|
800
|
+
status: finalStatus,
|
|
373
801
|
attempts,
|
|
374
802
|
primaryStatus,
|
|
375
803
|
compacted,
|
|
@@ -380,7 +808,24 @@ export async function createProxyServer(options = {}) {
|
|
|
380
808
|
...(upstreamError ? { upstreamError } : {}),
|
|
381
809
|
durationMs: Date.now() - startAt
|
|
382
810
|
});
|
|
383
|
-
|
|
811
|
+
if (debugDump) {
|
|
812
|
+
appendLog(logFile, {
|
|
813
|
+
kind: 'debug_dump',
|
|
814
|
+
method: req.method,
|
|
815
|
+
path: url.pathname,
|
|
816
|
+
upstream: url.toString(),
|
|
817
|
+
incomingHeaders,
|
|
818
|
+
normalizedHeaders: toHeaderObject(baseHeaders, { includeAuth: debugIncludeAuth }),
|
|
819
|
+
rawRequestBody: bufferToDebugText(rawBodyText || rawBody, debugMaxBody),
|
|
820
|
+
normalizedRequestBody: bufferToDebugText(normalizedBodyText || body, debugMaxBody),
|
|
821
|
+
attempts: debugAttempts,
|
|
822
|
+
finalResponse: {
|
|
823
|
+
status: finalStatus,
|
|
824
|
+
headers: finalHeaders,
|
|
825
|
+
body: bufferToDebugText(outBuffer, debugMaxBody)
|
|
826
|
+
}
|
|
827
|
+
});
|
|
828
|
+
}
|
|
384
829
|
} catch (err) {
|
|
385
830
|
const message = String(err?.message || err);
|
|
386
831
|
appendLog(logFile, {
|