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.
- package/README.md +2 -2
- package/README.zh.md +2 -2
- package/cli/builtin-proxy.js +408 -16
- package/cli/openai-bridge.js +504 -25
- package/package.json +1 -1
- package/web-ui/modules/app.methods.claude-config.mjs +2 -3
package/cli/openai-bridge.js
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
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:
|
|
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
|
|
1151
|
+
function normalizeResponsesPayloadForUpstream(payload, stream) {
|
|
1047
1152
|
const body = payload && typeof payload === 'object' ? payload : {};
|
|
1048
|
-
const normalized = { ...body, stream
|
|
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
|
-
|
|
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
|