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 CHANGED
@@ -54,7 +54,7 @@ Have you ever felt overwhelmed by managing multiple local AI agents? Each has it
54
54
 
55
55
  Unlike simple wrappers, Codex Mate acts as a **Local Agent Bridge**:
56
56
  - **Unified Session Browser**: Search, inspect, filter, and export local sessions across Codex, Claude Code, Gemini CLI, and CodeBuddy Code from one place.
57
- - **OpenAI-Compatible Bridge**: Use Codex with any OpenAI-compatible UI by normalizing the Responses API.
57
+ - **OpenAI-Compatible Bridge**: Use Codex with any OpenAI-compatible UI by normalizing the Responses API; the built-in Codex conversion also fills and normalizes Codex fingerprint headers such as `User-Agent`, `Version`, `OpenAI-Beta`, and `Originator` so upstream providers see an official Codex CLI-shaped request.
58
58
  - **Claude Provider Bridge**: Connect Claude Code to OpenAI Chat Completions-compatible providers and Ollama through the built-in local Claude-compatible proxy.
59
59
  - **OpenCode Provider Control**: Manage OpenCode provider/model selection with a CodexMate-owned provider store under `~/.codexmate`, projecting only the active provider into native OpenCode config to avoid polluting or deleting user-owned settings.
60
60
  - **Skills Marketplace**: A local-first market to share and import skills between different agent apps.
@@ -73,7 +73,7 @@ Unlike simple wrappers, Codex Mate acts as a **Local Agent Bridge**:
73
73
  | **Usage Analytics** | ✅ | Visualize message trends and top projects |
74
74
  | **Local Skills Market** | ✅ | Cross-app import/export of agent skills |
75
75
  | **Task Queue** | ✅ | DAG-based task execution and logs |
76
- | **OpenAI Bridge** | ✅ | Convert Codex Responses API to standard OpenAI format |
76
+ | **OpenAI Bridge** | ✅ | Convert Codex Responses API to standard OpenAI format and attach/normalize Codex fingerprints in the built-in conversion |
77
77
  | **Claude Provider Bridge** | ✅ | Connect Claude Code to OpenAI Chat Completions-compatible providers and Ollama via the built-in Claude-compatible proxy |
78
78
  | **OpenCode Provider Store** | ✅ | Keep multiple OpenCode providers in `~/.codexmate` while projecting only the selected provider to native OpenCode config |
79
79
  | **Prompt Templates** | ✅ | Reusable prompt plugins with variables |
package/README.zh.md CHANGED
@@ -54,7 +54,7 @@
54
54
 
55
55
  不同于简单的封装,Codex Mate 充当了 **本地智能体桥接器**:
56
56
  - **统一会话浏览器**:在一个地方检索、预览、筛选并导出 Codex、Claude Code、Gemini CLI 与 CodeBuddy Code 的本地会话。
57
- - **OpenAI 兼容桥接**:通过归一化 Responses API,让 Codex 能够与任何支持 OpenAI 格式的 UI 配合使用。
57
+ - **OpenAI 兼容桥接**:通过归一化 Responses API,让 Codex 能够与任何支持 OpenAI 格式的 UI 配合使用;内建 Codex 转换会补齐并规范化 `User-Agent`、`Version`、`OpenAI-Beta`、`Originator` 等 Codex 指纹,对上游伪装为官方 Codex CLI 请求。
58
58
  - **Claude Provider 桥接**:通过内建本地 Claude 兼容代理,让 Claude Code 接入 OpenAI Chat Completions 兼容 provider 与 Ollama。
59
59
  - **OpenCode Provider 控制**:在 `~/.codexmate` 下维护 CodexMate 自有的 OpenCode 多 provider 存储,只将当前选中的 provider 投影到 OpenCode 原生配置,避免污染或误删用户已有配置。
60
60
  - **Skills 市场**:本地优先的市场,支持在不同的智能体应用之间共享和导入 Skills。
@@ -73,7 +73,7 @@
73
73
  | **Usage 统计** | ✅ | 可视化消息趋势与热门项目统计 |
74
74
  | **本地 Skills 市场** | ✅ | 跨应用的智能体 Skills 导入与导出 |
75
75
  | **任务队列** | ✅ | 基于 DAG 的任务执行与日志查看 |
76
- | **OpenAI 桥接** | ✅ | 将 Codex Responses API 转换为标准 OpenAI 格式 |
76
+ | **OpenAI 桥接** | ✅ | 将 Codex Responses API 转换为标准 OpenAI 格式,并在内建转换中附加/规范化 Codex 指纹 |
77
77
  | **Claude Provider 桥接** | ✅ | 通过内建 Claude 兼容代理,让 Claude Code 接入 OpenAI Chat Completions 兼容 provider 与 Ollama |
78
78
  | **OpenCode Provider 存储** | ✅ | 在 `~/.codexmate` 中保留多个 OpenCode provider,只将当前选中的 provider 投影到 OpenCode 原生配置 |
79
79
  | **提示词模板** | ✅ | 支持变量的可复用提示词插件 |
@@ -66,6 +66,44 @@ function createBuiltinProxyRuntimeController(deps = {}) {
66
66
 
67
67
  let runtime = null;
68
68
 
69
+ const DEFAULT_CODEX_VERSION = '0.98.0';
70
+ const DEFAULT_CODEX_USER_AGENT = 'codex_cli_rs/0.98.0 (Mac OS 26.0.1; arm64) Apple_Terminal/464';
71
+ const DEFAULT_CODEX_ORIGINATOR = 'codex_cli_rs';
72
+ const DEFAULT_OPENAI_BETA = 'responses=experimental';
73
+
74
+ function firstHeaderValue(req, name) {
75
+ if (!req || !req.headers || typeof req.headers !== 'object') return '';
76
+ const value = req.headers[String(name || '').toLowerCase()];
77
+ if (Array.isArray(value)) return typeof value[0] === 'string' ? value[0] : '';
78
+ return typeof value === 'string' ? value : '';
79
+ }
80
+
81
+ function resolveCodexUserAgent(req) {
82
+ const incoming = firstHeaderValue(req, 'user-agent').trim();
83
+ if (/^(codex_cli_rs|codex-cli)\//i.test(incoming)) return incoming;
84
+ return DEFAULT_CODEX_USER_AGENT;
85
+ }
86
+
87
+ function resolveCodexOriginator() {
88
+ // Some Codex-only upstreams validate Originator separately from User-Agent.
89
+ // The local TUI may send values such as `codex-tui`, but upstream Codex
90
+ // gates commonly expect the official Rust CLI originator token.
91
+ return DEFAULT_CODEX_ORIGINATOR;
92
+ }
93
+
94
+ function codexUpstreamHeaders(req, upstream) {
95
+ const version = firstHeaderValue(req, 'version').trim() || DEFAULT_CODEX_VERSION;
96
+ const openaiBeta = firstHeaderValue(req, 'openai-beta').trim() || DEFAULT_OPENAI_BETA;
97
+ return {
98
+ ...(upstream && upstream.authHeader ? { 'Authorization': upstream.authHeader } : {}),
99
+ 'User-Agent': resolveCodexUserAgent(req),
100
+ 'Version': version,
101
+ 'OpenAI-Beta': openaiBeta,
102
+ 'Originator': resolveCodexOriginator(),
103
+ 'X-Codexmate-Proxy': '1'
104
+ };
105
+ }
106
+
69
107
  function readRequestBody(req, maxBytes) {
70
108
  return new Promise((resolve) => {
71
109
  let body = '';
@@ -902,7 +940,6 @@ function createBuiltinProxyRuntimeController(deps = {}) {
902
940
  'n',
903
941
  'modalities',
904
942
  'audio',
905
- 'reasoning',
906
943
  'reasoning_effort',
907
944
  'service_tier'
908
945
  ];
@@ -926,10 +963,15 @@ function createBuiltinProxyRuntimeController(deps = {}) {
926
963
  if (isRecord(source.text) && asTrimmedString(source.text.verbosity)) {
927
964
  chatBody.verbosity = asTrimmedString(source.text.verbosity);
928
965
  }
966
+ if (isRecord(source.reasoning) && asTrimmedString(source.reasoning.effort)) {
967
+ chatBody.reasoning_effort = asTrimmedString(source.reasoning.effort);
968
+ }
929
969
 
930
970
  pruneInvalidChatToolChoice(chatBody);
931
971
 
932
- if (Object.prototype.hasOwnProperty.call(source, 'max_tokens')) {
972
+ if (Object.prototype.hasOwnProperty.call(source, 'max_completion_tokens')) {
973
+ chatBody.max_tokens = Math.max(128, Number(source.max_completion_tokens) || 0);
974
+ } else if (Object.prototype.hasOwnProperty.call(source, 'max_tokens')) {
933
975
  chatBody.max_tokens = source.max_tokens;
934
976
  } else if (source.max_output_tokens != null) {
935
977
  chatBody.max_tokens = source.max_output_tokens;
@@ -938,6 +980,18 @@ function createBuiltinProxyRuntimeController(deps = {}) {
938
980
  return chatBody;
939
981
  }
940
982
 
983
+ function normalizeResponsesPayloadForUpstream(payload, stream) {
984
+ const source = isRecord(payload) ? payload : {};
985
+ const normalized = { ...source, stream };
986
+ if (isRecord(source.reasoning)) {
987
+ const include = Array.isArray(source.include) ? source.include.filter((item) => typeof item === 'string') : [];
988
+ if (!include.includes('reasoning.encrypted_content')) {
989
+ normalized.include = [...include, 'reasoning.encrypted_content'];
990
+ }
991
+ }
992
+ return normalized;
993
+ }
994
+
941
995
  function ensureResponseMetadata(payload) {
942
996
  const base = payload && typeof payload === 'object' && !Array.isArray(payload) ? payload : {};
943
997
  const id = typeof base.id === 'string' && base.id.trim()
@@ -963,6 +1017,112 @@ function createBuiltinProxyRuntimeController(deps = {}) {
963
1017
  res.write(`data: ${JSON.stringify(dataObj)}\n\n`);
964
1018
  }
965
1019
 
1020
+ function buildResponsesSseItem(item, fallbackId) {
1021
+ const source = isRecord(item) ? item : {};
1022
+ const itemId = asTrimmedString(source.id || source.call_id) || fallbackId || `item_${crypto.randomBytes(8).toString('hex')}`;
1023
+ if (source.type === 'message') {
1024
+ return {
1025
+ ...source,
1026
+ id: itemId,
1027
+ role: source.role || 'assistant',
1028
+ content: Array.isArray(source.content)
1029
+ ? source.content.map((part) => {
1030
+ if (!isRecord(part)) return part;
1031
+ if (part.type !== 'output_text') return cloneJsonValue(part);
1032
+ return {
1033
+ ...part,
1034
+ text: typeof part.text === 'string' ? part.text : '',
1035
+ annotations: Array.isArray(part.annotations) ? part.annotations : [],
1036
+ logprobs: Array.isArray(part.logprobs) ? part.logprobs : []
1037
+ };
1038
+ })
1039
+ : []
1040
+ };
1041
+ }
1042
+ if (source.type === 'reasoning') {
1043
+ return {
1044
+ ...source,
1045
+ id: itemId,
1046
+ summary: Array.isArray(source.summary) ? source.summary : []
1047
+ };
1048
+ }
1049
+ if (source.type === 'function_call') {
1050
+ return {
1051
+ ...source,
1052
+ id: itemId,
1053
+ call_id: asTrimmedString(source.call_id) || itemId,
1054
+ name: asTrimmedString(source.name),
1055
+ arguments: typeof source.arguments === 'string' ? source.arguments : ''
1056
+ };
1057
+ }
1058
+ if (source.type === 'custom_tool_call') {
1059
+ return {
1060
+ ...source,
1061
+ id: itemId,
1062
+ call_id: asTrimmedString(source.call_id) || itemId,
1063
+ name: asTrimmedString(source.name),
1064
+ input: typeof source.input === 'string' ? source.input : ''
1065
+ };
1066
+ }
1067
+ return { ...source, id: itemId };
1068
+ }
1069
+
1070
+ function emitResponsesTextPartAdded(res, itemId, outputIndex, contentIndex, nextSeq) {
1071
+ writeSse(res, 'response.content_part.added', {
1072
+ type: 'response.content_part.added',
1073
+ item_id: itemId,
1074
+ output_index: outputIndex,
1075
+ content_index: contentIndex,
1076
+ part: { type: 'output_text', text: '', annotations: [], logprobs: [] },
1077
+ sequence_number: nextSeq()
1078
+ });
1079
+ }
1080
+
1081
+ function emitResponsesTextPartDone(res, itemId, outputIndex, contentIndex, text, nextSeq) {
1082
+ writeSse(res, 'response.content_part.done', {
1083
+ type: 'response.content_part.done',
1084
+ item_id: itemId,
1085
+ output_index: outputIndex,
1086
+ content_index: contentIndex,
1087
+ part: { type: 'output_text', text: typeof text === 'string' ? text : '', annotations: [], logprobs: [] },
1088
+ sequence_number: nextSeq()
1089
+ });
1090
+ }
1091
+
1092
+ function emitResponsesToolArgumentEvents(res, item, outputIndex, nextSeq) {
1093
+ const eventType = item.type === 'custom_tool_call'
1094
+ ? 'response.custom_tool_call_input.delta'
1095
+ : 'response.function_call_arguments.delta';
1096
+ const doneType = item.type === 'custom_tool_call'
1097
+ ? ''
1098
+ : 'response.function_call_arguments.done';
1099
+ const value = item.type === 'custom_tool_call'
1100
+ ? (typeof item.input === 'string' ? item.input : '')
1101
+ : (typeof item.arguments === 'string' ? item.arguments : '');
1102
+ if (value) {
1103
+ writeSse(res, eventType, {
1104
+ type: eventType,
1105
+ item_id: item.id,
1106
+ output_index: outputIndex,
1107
+ call_id: item.call_id,
1108
+ name: item.name,
1109
+ delta: value,
1110
+ sequence_number: nextSeq()
1111
+ });
1112
+ }
1113
+ if (doneType) {
1114
+ writeSse(res, doneType, {
1115
+ type: doneType,
1116
+ item_id: item.id,
1117
+ output_index: outputIndex,
1118
+ call_id: item.call_id,
1119
+ name: item.name,
1120
+ arguments: value,
1121
+ sequence_number: nextSeq()
1122
+ });
1123
+ }
1124
+ }
1125
+
966
1126
  function sendResponsesSse(res, responsePayload) {
967
1127
  const response = ensureResponseMetadata(responsePayload);
968
1128
  const responseId = response.id;
@@ -989,12 +1149,14 @@ function createBuiltinProxyRuntimeController(deps = {}) {
989
1149
  const itemType = typeof item.type === 'string' ? item.type : '';
990
1150
  const itemId = typeof item.id === 'string' && item.id.trim()
991
1151
  ? item.id.trim()
992
- : `item_${crypto.randomBytes(8).toString('hex')}`;
1152
+ : (typeof item.call_id === 'string' && item.call_id.trim() ? item.call_id.trim() : `item_${crypto.randomBytes(8).toString('hex')}`);
1153
+ const wireItem = buildResponsesSseItem(item, itemId);
993
1154
 
994
1155
  writeSse(res, 'response.output_item.added', {
995
1156
  type: 'response.output_item.added',
996
1157
  output_index: outputIndex,
997
- item: { ...item, id: itemId }
1158
+ item: wireItem,
1159
+ sequence_number: nextSeq()
998
1160
  });
999
1161
 
1000
1162
  if (itemType === 'message') {
@@ -1004,6 +1166,7 @@ function createBuiltinProxyRuntimeController(deps = {}) {
1004
1166
  if (!block || typeof block !== 'object') continue;
1005
1167
  if (block.type !== 'output_text') continue;
1006
1168
  const text = typeof block.text === 'string' ? block.text : '';
1169
+ emitResponsesTextPartAdded(res, itemId, outputIndex, contentIndex, nextSeq);
1007
1170
  if (text) {
1008
1171
  writeSse(res, 'response.output_text.delta', {
1009
1172
  type: 'response.output_text.delta',
@@ -1022,13 +1185,40 @@ function createBuiltinProxyRuntimeController(deps = {}) {
1022
1185
  text,
1023
1186
  sequence_number: nextSeq()
1024
1187
  });
1188
+ emitResponsesTextPartDone(res, itemId, outputIndex, contentIndex, text, nextSeq);
1189
+ }
1190
+ } else if (itemType === 'function_call' || itemType === 'custom_tool_call') {
1191
+ emitResponsesToolArgumentEvents(res, wireItem, outputIndex, nextSeq);
1192
+ } else if (itemType === 'reasoning') {
1193
+ const summary = Array.isArray(item.summary) ? item.summary : [];
1194
+ for (let summaryIndex = 0; summaryIndex < summary.length; summaryIndex += 1) {
1195
+ const block = summary[summaryIndex];
1196
+ const text = block && typeof block.text === 'string' ? block.text : '';
1197
+ if (text) {
1198
+ writeSse(res, 'response.reasoning_summary_text.delta', {
1199
+ type: 'response.reasoning_summary_text.delta',
1200
+ item_id: itemId,
1201
+ output_index: outputIndex,
1202
+ summary_index: summaryIndex,
1203
+ delta: text,
1204
+ sequence_number: nextSeq()
1205
+ });
1206
+ }
1207
+ writeSse(res, 'response.reasoning_summary_text.done', {
1208
+ type: 'response.reasoning_summary_text.done',
1209
+ item_id: itemId,
1210
+ output_index: outputIndex,
1211
+ summary_index: summaryIndex,
1212
+ text,
1213
+ sequence_number: nextSeq()
1214
+ });
1025
1215
  }
1026
1216
  }
1027
1217
 
1028
1218
  writeSse(res, 'response.output_item.done', {
1029
1219
  type: 'response.output_item.done',
1030
1220
  output_index: outputIndex,
1031
- item: { ...item, id: itemId },
1221
+ item: wireItem,
1032
1222
  sequence_number: nextSeq()
1033
1223
  });
1034
1224
  }
@@ -1067,6 +1257,40 @@ function createBuiltinProxyRuntimeController(deps = {}) {
1067
1257
  const delta = choice && choice.delta && typeof choice.delta === 'object' ? choice.delta : null;
1068
1258
  if (!delta) continue;
1069
1259
 
1260
+ const reasoningSegment = typeof delta.reasoning_content === 'string'
1261
+ ? delta.reasoning_content
1262
+ : (typeof delta.reasoning === 'string'
1263
+ ? delta.reasoning
1264
+ : (typeof delta.reasoning_text === 'string' ? delta.reasoning_text : ''));
1265
+ if (reasoningSegment) {
1266
+ if (!state.reasoningItem) {
1267
+ state.reasoningItem = {
1268
+ id: `rs_${crypto.randomBytes(8).toString('hex')}`,
1269
+ type: 'reasoning',
1270
+ summary: [{ type: 'summary_text', text: '' }]
1271
+ };
1272
+ state.output.push(state.reasoningItem);
1273
+ state.outputStarted = true;
1274
+ beginChatStreamResponsesSse(state);
1275
+ writeSse(state.res, 'response.output_item.added', {
1276
+ type: 'response.output_item.added',
1277
+ output_index: state.output.length - 1,
1278
+ item: buildResponsesSseItem(state.reasoningItem, state.reasoningItem.id),
1279
+ sequence_number: state.nextSeq()
1280
+ });
1281
+ }
1282
+ state.reasoningText += reasoningSegment;
1283
+ state.reasoningItem.summary[0].text = state.reasoningText;
1284
+ writeSse(state.res, 'response.reasoning_summary_text.delta', {
1285
+ type: 'response.reasoning_summary_text.delta',
1286
+ item_id: state.reasoningItem.id,
1287
+ output_index: state.output.indexOf(state.reasoningItem),
1288
+ summary_index: 0,
1289
+ delta: reasoningSegment,
1290
+ sequence_number: state.nextSeq()
1291
+ });
1292
+ }
1293
+
1070
1294
  if (typeof delta.content === 'string' && delta.content) {
1071
1295
  if (!state.messageItem) {
1072
1296
  state.messageItem = {
@@ -1081,8 +1305,10 @@ function createBuiltinProxyRuntimeController(deps = {}) {
1081
1305
  writeSse(state.res, 'response.output_item.added', {
1082
1306
  type: 'response.output_item.added',
1083
1307
  output_index: state.output.length - 1,
1084
- item: state.messageItem
1308
+ item: buildResponsesSseItem(state.messageItem, state.messageItem.id),
1309
+ sequence_number: state.nextSeq()
1085
1310
  });
1311
+ emitResponsesTextPartAdded(state.res, state.messageItem.id, state.output.length - 1, 0, state.nextSeq);
1086
1312
  }
1087
1313
  state.messageText += delta.content;
1088
1314
  state.messageItem.content[0].text = state.messageText;
@@ -1134,6 +1360,24 @@ function createBuiltinProxyRuntimeController(deps = {}) {
1134
1360
  state.finished = true;
1135
1361
  stopChatStreamHeartbeat(state);
1136
1362
 
1363
+ if (state.reasoningItem) {
1364
+ const outputIndex = state.output.indexOf(state.reasoningItem);
1365
+ writeSse(state.res, 'response.reasoning_summary_text.done', {
1366
+ type: 'response.reasoning_summary_text.done',
1367
+ item_id: state.reasoningItem.id,
1368
+ output_index: outputIndex,
1369
+ summary_index: 0,
1370
+ text: state.reasoningText,
1371
+ sequence_number: state.nextSeq()
1372
+ });
1373
+ writeSse(state.res, 'response.output_item.done', {
1374
+ type: 'response.output_item.done',
1375
+ output_index: outputIndex,
1376
+ item: buildResponsesSseItem(state.reasoningItem, state.reasoningItem.id),
1377
+ sequence_number: state.nextSeq()
1378
+ });
1379
+ }
1380
+
1137
1381
  if (state.messageItem) {
1138
1382
  const outputIndex = state.output.indexOf(state.messageItem);
1139
1383
  writeSse(state.res, 'response.output_text.done', {
@@ -1144,10 +1388,11 @@ function createBuiltinProxyRuntimeController(deps = {}) {
1144
1388
  text: state.messageText,
1145
1389
  sequence_number: state.nextSeq()
1146
1390
  });
1391
+ emitResponsesTextPartDone(state.res, state.messageItem.id, outputIndex, 0, state.messageText, state.nextSeq);
1147
1392
  writeSse(state.res, 'response.output_item.done', {
1148
1393
  type: 'response.output_item.done',
1149
1394
  output_index: outputIndex,
1150
- item: state.messageItem,
1395
+ item: buildResponsesSseItem(state.messageItem, state.messageItem.id),
1151
1396
  sequence_number: state.nextSeq()
1152
1397
  });
1153
1398
  }
@@ -1162,12 +1407,14 @@ function createBuiltinProxyRuntimeController(deps = {}) {
1162
1407
  writeSse(state.res, 'response.output_item.added', {
1163
1408
  type: 'response.output_item.added',
1164
1409
  output_index: outputIndex,
1165
- item
1410
+ item: buildResponsesSseItem(item, item.call_id),
1411
+ sequence_number: state.nextSeq()
1166
1412
  });
1413
+ emitResponsesToolArgumentEvents(state.res, buildResponsesSseItem(item, item.call_id), outputIndex, state.nextSeq);
1167
1414
  writeSse(state.res, 'response.output_item.done', {
1168
1415
  type: 'response.output_item.done',
1169
1416
  output_index: outputIndex,
1170
- item,
1417
+ item: buildResponsesSseItem(item, item.call_id),
1171
1418
  sequence_number: state.nextSeq()
1172
1419
  });
1173
1420
  }
@@ -1244,6 +1491,8 @@ function createBuiltinProxyRuntimeController(deps = {}) {
1244
1491
  output: [],
1245
1492
  messageItem: null,
1246
1493
  messageText: '',
1494
+ reasoningItem: null,
1495
+ reasoningText: '',
1247
1496
  toolCalls: [],
1248
1497
  toolTypesByName: options.toolTypesByName || {},
1249
1498
  finished: false,
@@ -1427,6 +1676,127 @@ function createBuiltinProxyRuntimeController(deps = {}) {
1427
1676
  });
1428
1677
  }
1429
1678
 
1679
+ function streamResponsesSse(targetUrl, options = {}) {
1680
+ const parsed = new URL(targetUrl);
1681
+ const transport = parsed.protocol === 'https:' ? https : http;
1682
+ const bodyText = options.body ? JSON.stringify(options.body) : '';
1683
+ const headers = {
1684
+ 'Accept': 'text/event-stream',
1685
+ ...(options.body ? { 'Content-Type': 'application/json' } : {}),
1686
+ ...(options.headers || {})
1687
+ };
1688
+ if (options.body) {
1689
+ headers['Content-Length'] = Buffer.byteLength(bodyText, 'utf-8');
1690
+ }
1691
+ const timeoutMs = Number.isFinite(options.timeoutMs)
1692
+ ? Math.max(1000, Number(options.timeoutMs))
1693
+ : 30000;
1694
+ const res = options.res;
1695
+
1696
+ return new Promise((resolve) => {
1697
+ let settled = false;
1698
+ let streamAccepted = false;
1699
+ const finish = (value) => {
1700
+ if (settled) return;
1701
+ settled = true;
1702
+ resolve(value);
1703
+ };
1704
+ const req = transport.request({
1705
+ protocol: parsed.protocol,
1706
+ hostname: parsed.hostname,
1707
+ port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
1708
+ method: options.method || 'POST',
1709
+ path: `${parsed.pathname}${parsed.search}`,
1710
+ headers,
1711
+ agent: parsed.protocol === 'https:' ? HTTPS_KEEP_ALIVE_AGENT : HTTP_KEEP_ALIVE_AGENT
1712
+ }, (upstreamRes) => {
1713
+ const status = upstreamRes.statusCode || 0;
1714
+ const chunks = [];
1715
+ const contentType = String(upstreamRes.headers && upstreamRes.headers['content-type'] || '');
1716
+
1717
+ const collectBody = (done) => {
1718
+ upstreamRes.on('data', (chunk) => chunk && chunks.push(chunk));
1719
+ upstreamRes.on('end', () => done(chunks.length ? Buffer.concat(chunks).toString('utf-8') : ''));
1720
+ };
1721
+
1722
+ upstreamRes.on('error', (err) => finish({ ok: false, error: err && err.message ? err.message : 'upstream stream failed' }));
1723
+ upstreamRes.on('aborted', () => finish({ ok: false, retryTransient: true, error: 'upstream stream aborted' }));
1724
+
1725
+ if (status === 404 || status === 405) {
1726
+ collectBody((bodyTextResult) => finish({ retry: true, status, bodyText: bodyTextResult }));
1727
+ return;
1728
+ }
1729
+
1730
+ if (status >= 400) {
1731
+ collectBody((bodyTextResult) => finish({ ok: false, status, bodyText: bodyTextResult }));
1732
+ return;
1733
+ }
1734
+
1735
+ if (/text\/event-stream/i.test(contentType)) {
1736
+ streamAccepted = true;
1737
+ req.setTimeout(0);
1738
+ res.writeHead(200, {
1739
+ 'Content-Type': 'text/event-stream; charset=utf-8',
1740
+ 'Cache-Control': 'no-cache',
1741
+ 'Connection': 'keep-alive',
1742
+ 'X-Accel-Buffering': 'no'
1743
+ });
1744
+ upstreamRes.on('data', (chunk) => {
1745
+ if (chunk && !res.writableEnded) res.write(chunk);
1746
+ });
1747
+ upstreamRes.on('end', () => {
1748
+ if (!res.writableEnded) res.end();
1749
+ finish({ ok: true });
1750
+ });
1751
+ return;
1752
+ }
1753
+
1754
+ collectBody((text) => {
1755
+ const parsedJson = parseJsonOrError(text);
1756
+ res.writeHead(200, {
1757
+ 'Content-Type': 'text/event-stream; charset=utf-8',
1758
+ 'Cache-Control': 'no-cache',
1759
+ 'Connection': 'keep-alive',
1760
+ 'X-Accel-Buffering': 'no'
1761
+ });
1762
+ if (parsedJson.error) {
1763
+ writeSse(res, 'response.failed', { type: 'response.failed', error: `invalid upstream response: ${parsedJson.error}` });
1764
+ writeSse(res, 'done', '[DONE]');
1765
+ res.end();
1766
+ finish({ ok: true });
1767
+ return;
1768
+ }
1769
+ sendResponsesSse(res, ensureResponseMetadata(parsedJson.value));
1770
+ res.end();
1771
+ finish({ ok: true });
1772
+ });
1773
+ });
1774
+ req.setTimeout(timeoutMs, () => {
1775
+ if (streamAccepted) return;
1776
+ try { req.destroy(new Error('timeout')); } catch (_) {}
1777
+ finish({ ok: false, error: 'timeout' });
1778
+ });
1779
+ req.on('error', (err) => finish({ ok: false, error: err && err.message ? err.message : 'request failed' }));
1780
+ if (bodyText) req.write(bodyText);
1781
+ req.end();
1782
+ });
1783
+ }
1784
+
1785
+ async function streamResponsesSseWithFallbackUrls(baseUrl, pathSuffix, options = {}) {
1786
+ const urls = buildUpstreamUrlCandidates(baseUrl, pathSuffix);
1787
+ if (urls.length === 0) {
1788
+ return { ok: false, error: 'failed to build upstream URL' };
1789
+ }
1790
+ let lastResult = null;
1791
+ for (const url of urls) {
1792
+ const result = await retryTransientRequest(() => streamResponsesSse(url, options));
1793
+ lastResult = result;
1794
+ if (result && result.retry) continue;
1795
+ return result;
1796
+ }
1797
+ return lastResult || { ok: false, error: 'failed to build upstream URL' };
1798
+ }
1799
+
1430
1800
  async function streamChatCompletionsAsResponsesSseWithFallbackUrls(baseUrl, pathSuffix, options = {}) {
1431
1801
  const urls = buildUpstreamUrlCandidates(baseUrl, pathSuffix);
1432
1802
  if (urls.length === 0) {
@@ -1793,8 +2163,8 @@ function createBuiltinProxyRuntimeController(deps = {}) {
1793
2163
 
1794
2164
  // Responses shim:
1795
2165
  // - Codex CLI 默认走 /v1/responses(含 SSE)
1796
- // - SSE/streaming 任务优先走 chat/completions fallback,避免卡在会接收但不产出 Responses 的兼容网关
1797
- // - 非流式请求仍优先尝试 /v1/responses(stream=false),失败再转换到 chat/completions 并回包为 responses。
2166
+ // - SSE/streaming 任务优先保持 /v1/responses,避免 Codex-only upstream 把 fallback 后的 chat/completions 链路误判为非 Codex 客户端
2167
+ // - 仅在上游明确不支持 Responses 时,才转换到 chat/completions 并回包为 responses。
1798
2168
  if ((incomingPath === '/v1/responses' || incomingPath === '/v1/responses/') && (req.method || 'GET').toUpperCase() === 'POST') {
1799
2169
  void (async () => {
1800
2170
  const { body, error } = await readRequestBody(req, 10 * 1024 * 1024);
@@ -1813,16 +2183,38 @@ function createBuiltinProxyRuntimeController(deps = {}) {
1813
2183
  const payload = parsed.value && typeof parsed.value === 'object' ? parsed.value : {};
1814
2184
  const wantsStream = payload.stream === true;
1815
2185
 
1816
- const commonHeaders = {
1817
- ...(upstream.authHeader ? { 'Authorization': upstream.authHeader } : {}),
1818
- 'X-Codexmate-Proxy': '1'
1819
- };
2186
+ const commonHeaders = codexUpstreamHeaders(req, upstream);
1820
2187
 
1821
2188
  const model = typeof payload.model === 'string' ? payload.model : '';
1822
2189
  const chatBody = buildChatCompletionsBodyFromResponsesPayload(payload);
1823
2190
  const toolTypesByName = collectResponsesToolTypesByName(payload.tools);
1824
2191
 
1825
2192
  if (wantsStream) {
2193
+ const streamedResponses = await streamResponsesSseWithFallbackUrls(upstream.baseUrl, 'responses', {
2194
+ method: 'POST',
2195
+ headers: commonHeaders,
2196
+ timeoutMs,
2197
+ body: normalizeResponsesPayloadForUpstream(payload, true),
2198
+ res,
2199
+ model
2200
+ });
2201
+ if (streamedResponses.ok) return;
2202
+ const canFallbackToChat = streamedResponses.status
2203
+ ? shouldFallbackFromUpstreamResponses(streamedResponses.status, streamedResponses.bodyText)
2204
+ : false;
2205
+ if (!canFallbackToChat) {
2206
+ if (!res.headersSent) {
2207
+ const status = streamedResponses.status && streamedResponses.status >= 400 ? streamedResponses.status : 502;
2208
+ res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8' });
2209
+ res.end(streamedResponses.bodyText || JSON.stringify({ error: streamedResponses.error || 'proxy request failed' }));
2210
+ } else if (!res.writableEnded) {
2211
+ writeSse(res, 'response.failed', { type: 'response.failed', error: streamedResponses.error || streamedResponses.bodyText || 'proxy request failed' });
2212
+ writeSse(res, 'done', '[DONE]');
2213
+ res.end();
2214
+ }
2215
+ return;
2216
+ }
2217
+
1826
2218
  const streamingChatBody = { ...chatBody, stream: true };
1827
2219
  const streamed = await streamChatCompletionsAsResponsesSseWithFallbackUrls(upstream.baseUrl, 'chat/completions', {
1828
2220
  method: 'POST',
@@ -1850,7 +2242,7 @@ function createBuiltinProxyRuntimeController(deps = {}) {
1850
2242
  method: 'POST',
1851
2243
  headers: commonHeaders,
1852
2244
  timeoutMs,
1853
- body: { ...payload, stream: false }
2245
+ body: normalizeResponsesPayloadForUpstream(payload, false)
1854
2246
  });
1855
2247
 
1856
2248
  // 优先走上游 /responses(如果支持)。若上游报错且不是“端点不支持”,则直接透传错误。