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/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
|
| **提示词模板** | ✅ | 支持变量的可复用提示词插件 |
|
package/cli/builtin-proxy.js
CHANGED
|
@@ -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, '
|
|
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:
|
|
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:
|
|
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
|
|
1797
|
-
// -
|
|
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:
|
|
2245
|
+
body: normalizeResponsesPayloadForUpstream(payload, false)
|
|
1854
2246
|
});
|
|
1855
2247
|
|
|
1856
2248
|
// 优先走上游 /responses(如果支持)。若上游报错且不是“端点不支持”,则直接透传错误。
|