codexmate 0.0.52 → 0.0.54

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.
@@ -11,6 +11,10 @@ const SETTINGS_VERSION = 1;
11
11
  const STREAM_IDLE_TIMEOUT_MS = 5 * 60 * 1000;
12
12
  const REQUEST_TIMEOUT_MS = 5 * 60 * 1000;
13
13
  const RESPONSES_UNSUPPORTED_TTL_MS = 30 * 60 * 1000;
14
+ const DEFAULT_CODEX_VERSION = '0.98.0';
15
+ const DEFAULT_CODEX_USER_AGENT = 'codex_cli_rs/0.98.0 (Mac OS 26.0.1; arm64) Apple_Terminal/464';
16
+ const DEFAULT_CODEX_ORIGINATOR = 'codex_cli_rs';
17
+ const DEFAULT_OPENAI_BETA = 'responses=experimental';
14
18
 
15
19
  function normalizeText(value) {
16
20
  return typeof value === 'string' ? value.trim() : '';
@@ -21,6 +25,42 @@ function normalizeProviderName(value) {
21
25
  return normalizeText(value);
22
26
  }
23
27
 
28
+ function firstHeaderValue(req, name) {
29
+ if (!req || !req.headers || typeof req.headers !== 'object') return '';
30
+ const value = req.headers[String(name || '').toLowerCase()];
31
+ if (Array.isArray(value)) return typeof value[0] === 'string' ? value[0] : '';
32
+ return typeof value === 'string' ? value : '';
33
+ }
34
+
35
+ function resolveCodexUserAgent(req) {
36
+ const incoming = firstHeaderValue(req, 'user-agent').trim();
37
+ if (/^(codex_cli_rs|codex-cli)\//i.test(incoming)) return incoming;
38
+ return DEFAULT_CODEX_USER_AGENT;
39
+ }
40
+
41
+ function resolveCodexOriginator() {
42
+ // Some Codex-only upstreams validate Originator separately from User-Agent.
43
+ // The local TUI may send values such as `codex-tui`, but upstream Codex
44
+ // gates commonly expect the official Rust CLI originator token.
45
+ return DEFAULT_CODEX_ORIGINATOR;
46
+ }
47
+
48
+ function buildCodexBridgeHeaders(req, upstream, authHeader) {
49
+ const upstreamHeaders = upstream && upstream.headers && typeof upstream.headers === 'object' && !Array.isArray(upstream.headers)
50
+ ? upstream.headers
51
+ : {};
52
+ const version = firstHeaderValue(req, 'version').trim() || DEFAULT_CODEX_VERSION;
53
+ const openaiBeta = firstHeaderValue(req, 'openai-beta').trim() || DEFAULT_OPENAI_BETA;
54
+ return {
55
+ ...(authHeader ? { Authorization: authHeader } : {}),
56
+ ...upstreamHeaders,
57
+ 'User-Agent': resolveCodexUserAgent(req),
58
+ 'Version': version,
59
+ 'OpenAI-Beta': openaiBeta,
60
+ 'Originator': resolveCodexOriginator()
61
+ };
62
+ }
63
+
24
64
  function normalizeOpenaiUpstreamBaseUrl(rawValue) {
25
65
  const normalized = normalizeBaseUrl(rawValue);
26
66
  if (!normalized) return '';
@@ -260,9 +300,28 @@ function cloneJsonValue(value) {
260
300
  }
261
301
 
262
302
  function normalizeResponsesToolOutput(value) {
263
- if (typeof value === 'string') return value;
264
- if (value == null) return '';
265
- return stringifyJsonValue(value, '');
303
+ if (typeof value === 'string') return value || '(empty)';
304
+ if (value == null) return '(empty)';
305
+ return stringifyJsonValue(value, '(empty)') || '(empty)';
306
+ }
307
+
308
+ function chatContentToPlainText(content) {
309
+ if (typeof content === 'string') return content;
310
+ if (!Array.isArray(content)) return '';
311
+ return content
312
+ .map((item) => {
313
+ if (typeof item === 'string') return item;
314
+ if (!isRecord(item)) return '';
315
+ if (typeof item.text === 'string') return item.text;
316
+ if (typeof item.content === 'string') return item.content;
317
+ return '';
318
+ })
319
+ .join('');
320
+ }
321
+
322
+ function wrapReasoningContentForChat(content) {
323
+ const text = chatContentToPlainText(content).trim();
324
+ return text ? `<thinking>${text}</thinking>` : '';
266
325
  }
267
326
 
268
327
  function normalizeOpenAiToolArguments(value) {
@@ -490,7 +549,7 @@ function normalizeResponsesInputToChatMessages(input) {
490
549
 
491
550
  if (itemType === 'reasoning') {
492
551
  flushPendingToolCalls();
493
- const reasoningContent = toOpenAiMessageContent(item.summary != null ? item.summary : (item.content != null ? item.content : item));
552
+ const reasoningContent = wrapReasoningContentForChat(toOpenAiMessageContent(item.summary != null ? item.summary : (item.content != null ? item.content : item)));
494
553
  const reasoningSignature = asTrimmedString(item.encrypted_content || item.reasoning_signature);
495
554
  if (!hasOpenAiMessageContent(reasoningContent) && !reasoningSignature) return;
496
555
  const message = { role: 'assistant', content: reasoningContent };
@@ -849,6 +908,12 @@ function convertResponsesRequestToChatCompletions(payload) {
849
908
  top_p: Number.isFinite(body.top_p) ? Number(body.top_p) : undefined,
850
909
  max_tokens: Number.isFinite(maxOutputTokens) && maxOutputTokens > 0 ? maxOutputTokens : undefined
851
910
  };
911
+ if (isRecord(body.reasoning)) {
912
+ const effort = asTrimmedString(body.reasoning.effort);
913
+ if (effort) chat.reasoning_effort = effort;
914
+ } else if (asTrimmedString(body.reasoning_effort)) {
915
+ chat.reasoning_effort = asTrimmedString(body.reasoning_effort);
916
+ }
852
917
  if (Array.isArray(body.stop) && body.stop.length) {
853
918
  chat.stop = body.stop.filter((item) => typeof item === 'string' && item.trim());
854
919
  }
@@ -900,12 +965,22 @@ function buildResponsesPayloadFromChatResult(model, text, toolCalls, upstreamPay
900
965
  }
901
966
  }
902
967
 
968
+ const choice = upstreamPayload && typeof upstreamPayload === 'object' && Array.isArray(upstreamPayload.choices)
969
+ ? upstreamPayload.choices[0]
970
+ : null;
971
+ const finishReason = choice && typeof choice.finish_reason === 'string' ? choice.finish_reason : '';
972
+ const statusPatch = finishReason === 'length'
973
+ ? { status: 'incomplete', incomplete_details: { reason: 'max_output_tokens' } }
974
+ : (finishReason === 'content_filter'
975
+ ? { status: 'incomplete', incomplete_details: { reason: 'content_filter' } }
976
+ : { status: 'completed' });
977
+
903
978
  const payload = {
904
979
  id: responseId,
905
980
  object: 'response',
906
981
  model,
907
982
  created_at: createdAt,
908
- status: 'completed',
983
+ ...statusPatch,
909
984
  output,
910
985
  output_text: trimmedText
911
986
  };
@@ -977,12 +1052,14 @@ function sendResponsesSse(res, responsePayload) {
977
1052
  const itemId = typeof item.id === 'string' && item.id.trim()
978
1053
  ? item.id.trim()
979
1054
  : (typeof item.call_id === 'string' && item.call_id.trim() ? item.call_id.trim() : `item_${crypto.randomBytes(8).toString('hex')}`);
1055
+ const wireItem = buildResponsesSseItem(item, itemId);
980
1056
 
981
1057
  // Emit item added so Codex can anchor subsequent deltas by output_index/content_index/item_id.
982
1058
  writeSse(res, 'response.output_item.added', {
983
1059
  type: 'response.output_item.added',
984
1060
  output_index: outputIndex,
985
- item: { ...item, id: itemId }
1061
+ item: wireItem,
1062
+ sequence_number: nextSeq()
986
1063
  });
987
1064
 
988
1065
  if (itemType === 'message') {
@@ -992,6 +1069,7 @@ function sendResponsesSse(res, responsePayload) {
992
1069
  if (!block || typeof block !== 'object') continue;
993
1070
  if (block.type !== 'output_text') continue;
994
1071
  const text = typeof block.text === 'string' ? block.text : '';
1072
+ emitResponsesTextPartAdded(res, itemId, outputIndex, contentIndex, nextSeq);
995
1073
  if (text) {
996
1074
  writeSse(res, 'response.output_text.delta', {
997
1075
  type: 'response.output_text.delta',
@@ -1010,6 +1088,33 @@ function sendResponsesSse(res, responsePayload) {
1010
1088
  text,
1011
1089
  sequence_number: nextSeq()
1012
1090
  });
1091
+ emitResponsesTextPartDone(res, itemId, outputIndex, contentIndex, text, nextSeq);
1092
+ }
1093
+ } else if (itemType === 'function_call' || itemType === 'custom_tool_call') {
1094
+ emitResponsesToolArgumentEvents(res, wireItem, outputIndex, nextSeq);
1095
+ } else if (itemType === 'reasoning') {
1096
+ const summary = Array.isArray(item.summary) ? item.summary : [];
1097
+ for (let summaryIndex = 0; summaryIndex < summary.length; summaryIndex += 1) {
1098
+ const block = summary[summaryIndex];
1099
+ const text = block && typeof block.text === 'string' ? block.text : '';
1100
+ if (text) {
1101
+ writeSse(res, 'response.reasoning_summary_text.delta', {
1102
+ type: 'response.reasoning_summary_text.delta',
1103
+ item_id: itemId,
1104
+ output_index: outputIndex,
1105
+ summary_index: summaryIndex,
1106
+ delta: text,
1107
+ sequence_number: nextSeq()
1108
+ });
1109
+ }
1110
+ writeSse(res, 'response.reasoning_summary_text.done', {
1111
+ type: 'response.reasoning_summary_text.done',
1112
+ item_id: itemId,
1113
+ output_index: outputIndex,
1114
+ summary_index: summaryIndex,
1115
+ text,
1116
+ sequence_number: nextSeq()
1117
+ });
1013
1118
  }
1014
1119
  }
1015
1120
 
@@ -1017,7 +1122,7 @@ function sendResponsesSse(res, responsePayload) {
1017
1122
  writeSse(res, 'response.output_item.done', {
1018
1123
  type: 'response.output_item.done',
1019
1124
  output_index: outputIndex,
1020
- item: { ...item, id: itemId },
1125
+ item: wireItem,
1021
1126
  sequence_number: nextSeq()
1022
1127
  });
1023
1128
  }
@@ -1043,15 +1148,25 @@ function extractResponsesOutputText(payload) {
1043
1148
  return '';
1044
1149
  }
1045
1150
 
1046
- function toUpstreamNonStreamingResponsesPayload(payload) {
1151
+ function normalizeResponsesPayloadForUpstream(payload, stream) {
1047
1152
  const body = payload && typeof payload === 'object' ? payload : {};
1048
- const normalized = { ...body, stream: false };
1153
+ const normalized = { ...body, stream };
1049
1154
  if (Array.isArray(body.tools)) {
1050
1155
  normalized.tools = normalizeResponsesToolsForResponsesApi(body.tools);
1051
1156
  }
1157
+ if (isRecord(body.reasoning)) {
1158
+ const include = Array.isArray(body.include) ? body.include.filter((item) => typeof item === 'string') : [];
1159
+ if (!include.includes('reasoning.encrypted_content')) {
1160
+ normalized.include = [...include, 'reasoning.encrypted_content'];
1161
+ }
1162
+ }
1052
1163
  return normalized;
1053
1164
  }
1054
1165
 
1166
+ function toUpstreamNonStreamingResponsesPayload(payload) {
1167
+ return normalizeResponsesPayloadForUpstream(payload, false);
1168
+ }
1169
+
1055
1170
  function shouldFallbackFromUpstreamResponses(status, bodyText) {
1056
1171
  if (!Number.isFinite(status)) return false;
1057
1172
  // Common "unsupported" status codes for a route.
@@ -1162,6 +1277,101 @@ function writeSse(res, eventName, dataObj) {
1162
1277
  res.write(`data: ${JSON.stringify(dataObj)}\n\n`);
1163
1278
  }
1164
1279
 
1280
+ function buildResponsesSseItem(item, fallbackId) {
1281
+ const source = isRecord(item) ? item : {};
1282
+ const itemId = asTrimmedString(source.id || source.call_id) || fallbackId || `item_${crypto.randomBytes(8).toString('hex')}`;
1283
+ if (source.type === 'message') {
1284
+ return {
1285
+ ...source,
1286
+ id: itemId,
1287
+ role: source.role || 'assistant',
1288
+ content: Array.isArray(source.content) ? source.content : []
1289
+ };
1290
+ }
1291
+ if (source.type === 'reasoning') {
1292
+ return {
1293
+ ...source,
1294
+ id: itemId,
1295
+ summary: Array.isArray(source.summary) ? source.summary : []
1296
+ };
1297
+ }
1298
+ if (source.type === 'function_call') {
1299
+ return {
1300
+ ...source,
1301
+ id: itemId,
1302
+ call_id: asTrimmedString(source.call_id) || itemId,
1303
+ name: asTrimmedString(source.name),
1304
+ arguments: typeof source.arguments === 'string' ? source.arguments : ''
1305
+ };
1306
+ }
1307
+ if (source.type === 'custom_tool_call') {
1308
+ return {
1309
+ ...source,
1310
+ id: itemId,
1311
+ call_id: asTrimmedString(source.call_id) || itemId,
1312
+ name: asTrimmedString(source.name),
1313
+ input: typeof source.input === 'string' ? source.input : ''
1314
+ };
1315
+ }
1316
+ return { ...source, id: itemId };
1317
+ }
1318
+
1319
+ function emitResponsesTextPartAdded(res, itemId, outputIndex, contentIndex, nextSeq) {
1320
+ writeSse(res, 'response.content_part.added', {
1321
+ type: 'response.content_part.added',
1322
+ item_id: itemId,
1323
+ output_index: outputIndex,
1324
+ content_index: contentIndex,
1325
+ part: { type: 'output_text', text: '', annotations: [], logprobs: [] },
1326
+ sequence_number: nextSeq()
1327
+ });
1328
+ }
1329
+
1330
+ function emitResponsesTextPartDone(res, itemId, outputIndex, contentIndex, text, nextSeq) {
1331
+ writeSse(res, 'response.content_part.done', {
1332
+ type: 'response.content_part.done',
1333
+ item_id: itemId,
1334
+ output_index: outputIndex,
1335
+ content_index: contentIndex,
1336
+ part: { type: 'output_text', text: typeof text === 'string' ? text : '', annotations: [], logprobs: [] },
1337
+ sequence_number: nextSeq()
1338
+ });
1339
+ }
1340
+
1341
+ function emitResponsesToolArgumentEvents(res, item, outputIndex, nextSeq) {
1342
+ const eventType = item.type === 'custom_tool_call'
1343
+ ? 'response.custom_tool_call_input.delta'
1344
+ : 'response.function_call_arguments.delta';
1345
+ const doneType = item.type === 'custom_tool_call'
1346
+ ? ''
1347
+ : 'response.function_call_arguments.done';
1348
+ const value = item.type === 'custom_tool_call'
1349
+ ? (typeof item.input === 'string' ? item.input : '')
1350
+ : (typeof item.arguments === 'string' ? item.arguments : '');
1351
+ if (value) {
1352
+ writeSse(res, eventType, {
1353
+ type: eventType,
1354
+ item_id: item.id,
1355
+ output_index: outputIndex,
1356
+ call_id: item.call_id,
1357
+ name: item.name,
1358
+ delta: value,
1359
+ sequence_number: nextSeq()
1360
+ });
1361
+ }
1362
+ if (doneType) {
1363
+ writeSse(res, doneType, {
1364
+ type: doneType,
1365
+ item_id: item.id,
1366
+ output_index: outputIndex,
1367
+ call_id: item.call_id,
1368
+ name: item.name,
1369
+ arguments: value,
1370
+ sequence_number: nextSeq()
1371
+ });
1372
+ }
1373
+ }
1374
+
1165
1375
  function appendChatStreamToolCall(target, toolCall) {
1166
1376
  if (!toolCall || typeof toolCall !== 'object') return;
1167
1377
  const index = Number.isFinite(toolCall.index) ? toolCall.index : target.length;
@@ -1192,6 +1402,37 @@ function writeChatCompletionChunkAsResponsesSse(state, chunk) {
1192
1402
  const delta = choice && choice.delta && typeof choice.delta === 'object' ? choice.delta : null;
1193
1403
  if (!delta) continue;
1194
1404
 
1405
+ const reasoningSegment = typeof delta.reasoning_content === 'string'
1406
+ ? delta.reasoning_content
1407
+ : (typeof delta.reasoning === 'string'
1408
+ ? delta.reasoning
1409
+ : (typeof delta.reasoning_text === 'string' ? delta.reasoning_text : ''));
1410
+ if (reasoningSegment) {
1411
+ if (!state.reasoningItem) {
1412
+ state.reasoningItem = {
1413
+ id: `rs_${crypto.randomBytes(8).toString('hex')}`,
1414
+ type: 'reasoning',
1415
+ summary: [{ type: 'summary_text', text: '' }]
1416
+ };
1417
+ state.output.push(state.reasoningItem);
1418
+ writeSse(state.res, 'response.output_item.added', {
1419
+ type: 'response.output_item.added',
1420
+ output_index: state.output.length - 1,
1421
+ item: buildResponsesSseItem(state.reasoningItem, state.reasoningItem.id)
1422
+ });
1423
+ }
1424
+ state.reasoningText += reasoningSegment;
1425
+ state.reasoningItem.summary[0].text = state.reasoningText;
1426
+ writeSse(state.res, 'response.reasoning_summary_text.delta', {
1427
+ type: 'response.reasoning_summary_text.delta',
1428
+ item_id: state.reasoningItem.id,
1429
+ output_index: state.output.indexOf(state.reasoningItem),
1430
+ summary_index: 0,
1431
+ delta: reasoningSegment,
1432
+ sequence_number: state.nextSeq()
1433
+ });
1434
+ }
1435
+
1195
1436
  const segments = [];
1196
1437
  // DeepSeek-style OpenAI-compatible streams may emit private reasoning in
1197
1438
  // `reasoning_content` before the final answer. Responses `output_text`
@@ -1212,8 +1453,9 @@ function writeChatCompletionChunkAsResponsesSse(state, chunk) {
1212
1453
  writeSse(state.res, 'response.output_item.added', {
1213
1454
  type: 'response.output_item.added',
1214
1455
  output_index: state.output.length - 1,
1215
- item: state.messageItem
1456
+ item: buildResponsesSseItem(state.messageItem, state.messageItem.id)
1216
1457
  });
1458
+ emitResponsesTextPartAdded(state.res, state.messageItem.id, state.output.length - 1, 0, state.nextSeq);
1217
1459
  }
1218
1460
  state.messageText += seg;
1219
1461
  state.messageItem.content[0].text = state.messageText;
@@ -1243,6 +1485,24 @@ function finishChatStreamResponsesSse(state) {
1243
1485
  if (!state || state.finished) return;
1244
1486
  state.finished = true;
1245
1487
 
1488
+ if (state.reasoningItem) {
1489
+ const outputIndex = state.output.indexOf(state.reasoningItem);
1490
+ writeSse(state.res, 'response.reasoning_summary_text.done', {
1491
+ type: 'response.reasoning_summary_text.done',
1492
+ item_id: state.reasoningItem.id,
1493
+ output_index: outputIndex,
1494
+ summary_index: 0,
1495
+ text: state.reasoningText,
1496
+ sequence_number: state.nextSeq()
1497
+ });
1498
+ writeSse(state.res, 'response.output_item.done', {
1499
+ type: 'response.output_item.done',
1500
+ output_index: outputIndex,
1501
+ item: buildResponsesSseItem(state.reasoningItem, state.reasoningItem.id),
1502
+ sequence_number: state.nextSeq()
1503
+ });
1504
+ }
1505
+
1246
1506
  if (state.messageItem) {
1247
1507
  const outputIndex = state.output.indexOf(state.messageItem);
1248
1508
  writeSse(state.res, 'response.output_text.done', {
@@ -1253,10 +1513,11 @@ function finishChatStreamResponsesSse(state) {
1253
1513
  text: state.messageText,
1254
1514
  sequence_number: state.nextSeq()
1255
1515
  });
1516
+ emitResponsesTextPartDone(state.res, state.messageItem.id, outputIndex, 0, state.messageText, state.nextSeq);
1256
1517
  writeSse(state.res, 'response.output_item.done', {
1257
1518
  type: 'response.output_item.done',
1258
1519
  output_index: outputIndex,
1259
- item: state.messageItem,
1520
+ item: buildResponsesSseItem(state.messageItem, state.messageItem.id),
1260
1521
  sequence_number: state.nextSeq()
1261
1522
  });
1262
1523
  }
@@ -1270,12 +1531,13 @@ function finishChatStreamResponsesSse(state) {
1270
1531
  writeSse(state.res, 'response.output_item.added', {
1271
1532
  type: 'response.output_item.added',
1272
1533
  output_index: outputIndex,
1273
- item
1534
+ item: buildResponsesSseItem(item, item.call_id)
1274
1535
  });
1536
+ emitResponsesToolArgumentEvents(state.res, buildResponsesSseItem(item, item.call_id), outputIndex, state.nextSeq);
1275
1537
  writeSse(state.res, 'response.output_item.done', {
1276
1538
  type: 'response.output_item.done',
1277
1539
  output_index: outputIndex,
1278
- item,
1540
+ item: buildResponsesSseItem(item, item.call_id),
1279
1541
  sequence_number: state.nextSeq()
1280
1542
  });
1281
1543
  }
@@ -1445,6 +1707,8 @@ function streamChatCompletionsAsResponsesSse(targetUrl, options = {}) {
1445
1707
  output: [],
1446
1708
  messageItem: null,
1447
1709
  messageText: '',
1710
+ reasoningItem: null,
1711
+ reasoningText: '',
1448
1712
  toolCalls: [],
1449
1713
  toolTypesByName: options.toolTypesByName || {},
1450
1714
  finished: false,
@@ -1533,6 +1797,184 @@ function streamChatCompletionsAsResponsesSse(targetUrl, options = {}) {
1533
1797
  });
1534
1798
  }
1535
1799
 
1800
+ function streamResponsesSse(targetUrl, options = {}) {
1801
+ const parsed = new URL(targetUrl);
1802
+ const transport = parsed.protocol === 'https:' ? https : http;
1803
+ const bodyText = options.body ? JSON.stringify(options.body) : '';
1804
+ const maxBytes = Number.isFinite(options.maxBytes) && options.maxBytes > 0
1805
+ ? Math.floor(options.maxBytes)
1806
+ : 0;
1807
+ const headers = {
1808
+ 'Accept': 'text/event-stream',
1809
+ ...(options.body ? { 'Content-Type': 'application/json' } : {}),
1810
+ ...(options.headers || {})
1811
+ };
1812
+ if (options.body) {
1813
+ headers['Content-Length'] = Buffer.byteLength(bodyText, 'utf-8');
1814
+ }
1815
+ const timeoutMs = Number.isFinite(options.timeoutMs)
1816
+ ? Math.max(1000, Number(options.timeoutMs))
1817
+ : STREAM_IDLE_TIMEOUT_MS;
1818
+ const res = options.res;
1819
+
1820
+ return new Promise((resolve) => {
1821
+ let settled = false;
1822
+ let upstreamReq = null;
1823
+ let streamAccepted = false;
1824
+ let streamIdleTimer = null;
1825
+ const finish = (value) => {
1826
+ if (settled) return;
1827
+ settled = true;
1828
+ if (streamIdleTimer) {
1829
+ clearTimeout(streamIdleTimer);
1830
+ streamIdleTimer = null;
1831
+ }
1832
+ resolve(value);
1833
+ };
1834
+ const abortUpstream = () => {
1835
+ if (upstreamReq) {
1836
+ try { upstreamReq.destroy(new Error('client aborted')); } catch (_) {}
1837
+ }
1838
+ };
1839
+ if (res && typeof res.once === 'function') {
1840
+ res.once('close', abortUpstream);
1841
+ }
1842
+
1843
+ upstreamReq = transport.request({
1844
+ protocol: parsed.protocol,
1845
+ hostname: parsed.hostname,
1846
+ port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
1847
+ method: options.method || 'POST',
1848
+ path: `${parsed.pathname}${parsed.search}`,
1849
+ headers,
1850
+ agent: parsed.protocol === 'https:' ? options.httpsAgent : options.httpAgent
1851
+ }, (upstreamRes) => {
1852
+ const status = upstreamRes.statusCode || 0;
1853
+ const chunks = [];
1854
+ let size = 0;
1855
+ const contentType = String(upstreamRes.headers && upstreamRes.headers['content-type'] || '');
1856
+ const collectChunk = (chunk) => {
1857
+ if (!chunk) return true;
1858
+ if (maxBytes > 0) {
1859
+ size += chunk.length;
1860
+ if (size > maxBytes) {
1861
+ chunks.length = 0;
1862
+ try { upstreamRes.destroy(new Error('response too large')); } catch (_) {}
1863
+ try { upstreamReq.destroy(new Error('response too large')); } catch (_) {}
1864
+ finish({ ok: false, status, error: 'response too large' });
1865
+ return false;
1866
+ }
1867
+ }
1868
+ chunks.push(chunk);
1869
+ return true;
1870
+ };
1871
+ const bodyFromChunks = () => (chunks.length ? Buffer.concat(chunks).toString('utf-8') : '');
1872
+
1873
+ if (status === 404 || status === 405) {
1874
+ upstreamRes.on('data', collectChunk);
1875
+ upstreamRes.on('end', () => finish({ retry: true, status, bodyText: bodyFromChunks() }));
1876
+ return;
1877
+ }
1878
+
1879
+ if (status >= 400) {
1880
+ upstreamRes.on('data', collectChunk);
1881
+ upstreamRes.on('end', () => finish({ ok: false, status, bodyText: bodyFromChunks() }));
1882
+ return;
1883
+ }
1884
+
1885
+ if (/text\/event-stream/i.test(contentType)) {
1886
+ streamAccepted = true;
1887
+ upstreamReq.setTimeout(0);
1888
+ const failAcceptedStream = (message) => {
1889
+ if (!res.writableEnded && !res.destroyed) {
1890
+ writeSse(res, 'response.failed', { type: 'response.failed', error: message });
1891
+ writeSse(res, 'done', '[DONE]');
1892
+ res.end();
1893
+ }
1894
+ try { upstreamRes.destroy(new Error(message)); } catch (_) {}
1895
+ try { upstreamReq.destroy(new Error(message)); } catch (_) {}
1896
+ finish({ ok: true });
1897
+ };
1898
+ const resetStreamIdleTimer = () => {
1899
+ if (streamIdleTimer) clearTimeout(streamIdleTimer);
1900
+ streamIdleTimer = setTimeout(() => {
1901
+ failAcceptedStream('upstream stream idle timeout');
1902
+ }, timeoutMs);
1903
+ if (typeof streamIdleTimer.unref === 'function') streamIdleTimer.unref();
1904
+ };
1905
+ if (!res.headersSent) {
1906
+ res.writeHead(200, {
1907
+ 'Content-Type': 'text/event-stream; charset=utf-8',
1908
+ 'Cache-Control': 'no-cache',
1909
+ 'Connection': 'keep-alive',
1910
+ 'X-Accel-Buffering': 'no'
1911
+ });
1912
+ if (typeof res.flushHeaders === 'function') res.flushHeaders();
1913
+ }
1914
+ resetStreamIdleTimer();
1915
+ upstreamRes.on('data', (chunk) => {
1916
+ resetStreamIdleTimer();
1917
+ if (chunk && !res.writableEnded && !res.destroyed) res.write(chunk);
1918
+ });
1919
+ upstreamRes.on('end', () => {
1920
+ if (!res.writableEnded && !res.destroyed) res.end();
1921
+ finish({ ok: true });
1922
+ });
1923
+ upstreamRes.on('aborted', () => {
1924
+ if (!res.writableEnded && !res.destroyed) {
1925
+ writeSse(res, 'response.failed', { type: 'response.failed', error: 'upstream stream aborted' });
1926
+ writeSse(res, 'done', '[DONE]');
1927
+ res.end();
1928
+ }
1929
+ finish({ ok: true });
1930
+ });
1931
+ upstreamRes.on('error', (err) => {
1932
+ if (!res.writableEnded && !res.destroyed) {
1933
+ writeSse(res, 'response.failed', { type: 'response.failed', error: err && err.message ? err.message : 'upstream stream failed' });
1934
+ writeSse(res, 'done', '[DONE]');
1935
+ res.end();
1936
+ }
1937
+ finish({ ok: true });
1938
+ });
1939
+ return;
1940
+ }
1941
+
1942
+ upstreamRes.on('data', collectChunk);
1943
+ upstreamRes.on('end', () => {
1944
+ const text = bodyFromChunks();
1945
+ const parsedJson = parseJsonOrError(text);
1946
+ if (!res.headersSent) {
1947
+ res.writeHead(200, {
1948
+ 'Content-Type': 'text/event-stream; charset=utf-8',
1949
+ 'Cache-Control': 'no-cache',
1950
+ 'Connection': 'keep-alive',
1951
+ 'X-Accel-Buffering': 'no'
1952
+ });
1953
+ if (typeof res.flushHeaders === 'function') res.flushHeaders();
1954
+ }
1955
+ if (parsedJson.error) {
1956
+ writeSse(res, 'response.failed', { type: 'response.failed', error: `invalid upstream response: ${parsedJson.error}` });
1957
+ writeSse(res, 'done', '[DONE]');
1958
+ if (!res.writableEnded && !res.destroyed) res.end();
1959
+ finish({ ok: true });
1960
+ return;
1961
+ }
1962
+ sendResponsesSse(res, ensureResponseMetadata(parsedJson.value));
1963
+ if (!res.writableEnded && !res.destroyed) res.end();
1964
+ finish({ ok: true });
1965
+ });
1966
+ });
1967
+ upstreamReq.setTimeout(timeoutMs, () => {
1968
+ if (streamAccepted) return;
1969
+ try { upstreamReq.destroy(new Error('timeout')); } catch (_) {}
1970
+ finish({ ok: false, error: 'timeout' });
1971
+ });
1972
+ upstreamReq.on('error', (err) => finish({ ok: false, error: err && err.message ? err.message : 'request failed' }));
1973
+ if (bodyText) upstreamReq.write(bodyText);
1974
+ upstreamReq.end();
1975
+ });
1976
+ }
1977
+
1536
1978
  async function proxyRequestJson(targetUrl, options = {}) {
1537
1979
  const parsed = new URL(targetUrl);
1538
1980
  const transport = parsed.protocol === 'https:' ? https : http;
@@ -1618,6 +2060,9 @@ function createOpenaiBridgeHttpHandler(options = {}) {
1618
2060
  const maxUpstreamBytes = Number.isFinite(options.maxUpstreamBytes) && options.maxUpstreamBytes > 0
1619
2061
  ? Math.floor(options.maxUpstreamBytes)
1620
2062
  : Math.max(16 * 1024 * 1024, maxBodySize > 0 ? maxBodySize * 4 : 0);
2063
+ const streamTimeoutMs = Number.isFinite(options.streamTimeoutMs) && options.streamTimeoutMs > 0
2064
+ ? Math.floor(options.streamTimeoutMs)
2065
+ : STREAM_IDLE_TIMEOUT_MS;
1621
2066
 
1622
2067
  if (!settingsFile) {
1623
2068
  throw new Error('createOpenaiBridgeHttpHandler 缺少 settingsFile');
@@ -1708,6 +2153,7 @@ function createOpenaiBridgeHttpHandler(options = {}) {
1708
2153
  const upstreamHeaders = upstream && upstream.headers && typeof upstream.headers === 'object' && !Array.isArray(upstream.headers)
1709
2154
  ? upstream.headers
1710
2155
  : {};
2156
+ const codexHeaders = buildCodexBridgeHeaders(req, upstream, authHeader);
1711
2157
 
1712
2158
  if (!normalizedSuffix) {
1713
2159
  if ((req.method || 'GET').toUpperCase() !== 'GET') {
@@ -1784,6 +2230,47 @@ function createOpenaiBridgeHttpHandler(options = {}) {
1784
2230
  const wantsSse = /text\/event-stream/i.test(String(acceptHeader || ''));
1785
2231
 
1786
2232
  if (streamRequested && wantsSse) {
2233
+ const upstreamResponsesUrl = joinApiUrl(upstream.baseUrl, 'responses');
2234
+ const skipResponsesProbe = isResponsesKnownUnsupported(upstream.baseUrl);
2235
+ const streamedResponses = skipResponsesProbe
2236
+ ? { ok: false, status: 404, bodyText: '' }
2237
+ : await retryTransientRequest(() => streamResponsesSse(upstreamResponsesUrl, {
2238
+ method: 'POST',
2239
+ body: normalizeResponsesPayloadForUpstream(responsesRequest, true),
2240
+ headers: codexHeaders,
2241
+ timeoutMs: streamTimeoutMs,
2242
+ maxBytes: maxUpstreamBytes,
2243
+ httpAgent,
2244
+ httpsAgent,
2245
+ res
2246
+ }));
2247
+
2248
+ if (streamedResponses.ok) {
2249
+ clearResponsesUnsupported(upstream.baseUrl);
2250
+ return;
2251
+ }
2252
+
2253
+ const canFallbackToChat = streamedResponses.status
2254
+ ? shouldFallbackFromUpstreamResponses(streamedResponses.status, streamedResponses.bodyText)
2255
+ : false;
2256
+ if (!canFallbackToChat) {
2257
+ if (res.writableEnded || res.destroyed) return;
2258
+ const status = streamedResponses.status && streamedResponses.status >= 400 ? streamedResponses.status : 502;
2259
+ if (!res.headersSent) {
2260
+ res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8' });
2261
+ res.end(streamedResponses.bodyText || JSON.stringify({ error: streamedResponses.error || 'Upstream request failed' }));
2262
+ } else {
2263
+ writeSse(res, 'response.failed', { type: 'response.failed', error: streamedResponses.error || streamedResponses.bodyText || 'Upstream request failed' });
2264
+ writeSse(res, 'done', '[DONE]');
2265
+ res.end();
2266
+ }
2267
+ return;
2268
+ }
2269
+
2270
+ if (!skipResponsesProbe && isResponsesEndpointUnsupported(streamedResponses.status, streamedResponses.bodyText)) {
2271
+ markResponsesUnsupported(upstream.baseUrl);
2272
+ }
2273
+
1787
2274
  const converted = convertResponsesRequestToChatCompletions(responsesRequest);
1788
2275
  if (converted.error) {
1789
2276
  res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
@@ -1795,10 +2282,8 @@ function createOpenaiBridgeHttpHandler(options = {}) {
1795
2282
  const streamed = await retryTransientRequest(() => streamChatCompletionsAsResponsesSse(upstreamUrl, {
1796
2283
  method: 'POST',
1797
2284
  body: chatBody,
1798
- headers: {
1799
- ...(authHeader ? { Authorization: authHeader } : {}),
1800
- ...upstreamHeaders
1801
- },
2285
+ headers: codexHeaders,
2286
+ timeoutMs: streamTimeoutMs,
1802
2287
  maxBytes: maxUpstreamBytes,
1803
2288
  httpAgent,
1804
2289
  httpsAgent,
@@ -1832,10 +2317,7 @@ function createOpenaiBridgeHttpHandler(options = {}) {
1832
2317
  : await retryTransientRequest(() => proxyRequestJson(upstreamResponsesUrl, {
1833
2318
  method: 'POST',
1834
2319
  body: toUpstreamNonStreamingResponsesPayload(responsesRequest),
1835
- headers: {
1836
- ...(authHeader ? { Authorization: authHeader } : {}),
1837
- ...upstreamHeaders
1838
- },
2320
+ headers: codexHeaders,
1839
2321
  maxBytes: maxUpstreamBytes,
1840
2322
  httpAgent,
1841
2323
  httpsAgent
@@ -1897,10 +2379,7 @@ function createOpenaiBridgeHttpHandler(options = {}) {
1897
2379
  const upstreamResult = await retryTransientRequest(() => proxyRequestJson(upstreamUrl, {
1898
2380
  method: 'POST',
1899
2381
  body: converted.chat,
1900
- headers: {
1901
- ...(authHeader ? { Authorization: authHeader } : {}),
1902
- ...upstreamHeaders
1903
- },
2382
+ headers: codexHeaders,
1904
2383
  maxBytes: maxUpstreamBytes,
1905
2384
  httpAgent,
1906
2385
  httpsAgent