codexmate 0.0.27 → 0.0.28

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.
Files changed (137) hide show
  1. package/README.md +421 -421
  2. package/README.zh.md +354 -354
  3. package/cli/agents-files.js +224 -224
  4. package/cli/archive-helpers.js +446 -446
  5. package/cli/auth-profiles.js +375 -375
  6. package/cli/builtin-proxy.js +1620 -1299
  7. package/cli/claude-proxy.js +1022 -1022
  8. package/cli/config-bootstrap.js +384 -384
  9. package/cli/config-health.js +338 -338
  10. package/cli/doctor-core.js +903 -903
  11. package/cli/import-skills-url.js +356 -356
  12. package/cli/openai-bridge.js +1489 -1091
  13. package/cli/openclaw-config.js +629 -629
  14. package/cli/session-convert-args.js +65 -65
  15. package/cli/session-convert-io.js +82 -82
  16. package/cli/session-convert.js +43 -43
  17. package/cli/session-usage.concurrent.js +28 -28
  18. package/cli/session-usage.js +118 -118
  19. package/cli/session-usage.models.js +176 -176
  20. package/cli/skills.js +1141 -1141
  21. package/cli/zip-commands.js +510 -510
  22. package/cli.js +15264 -15251
  23. package/lib/automation.js +404 -404
  24. package/lib/cli-file-utils.js +151 -151
  25. package/lib/cli-models-utils.js +440 -379
  26. package/lib/cli-network-utils.js +190 -190
  27. package/lib/cli-path-utils.js +85 -85
  28. package/lib/cli-session-utils.js +121 -121
  29. package/lib/cli-sessions.js +417 -417
  30. package/lib/cli-utils.js +155 -155
  31. package/lib/download-artifacts.js +92 -92
  32. package/lib/mcp-stdio.js +453 -453
  33. package/lib/task-orchestrator.js +869 -869
  34. package/lib/text-diff.js +303 -303
  35. package/lib/workflow-engine.js +340 -340
  36. package/package.json +3 -1
  37. package/plugins/README.md +20 -20
  38. package/plugins/README.zh-CN.md +20 -20
  39. package/plugins/prompt-templates/comment-polish/index.mjs +25 -25
  40. package/plugins/prompt-templates/computed.mjs +253 -253
  41. package/plugins/prompt-templates/index.mjs +8 -8
  42. package/plugins/prompt-templates/manifest.mjs +15 -15
  43. package/plugins/prompt-templates/methods.mjs +553 -619
  44. package/plugins/prompt-templates/overview.mjs +91 -90
  45. package/plugins/prompt-templates/ownership.mjs +19 -19
  46. package/plugins/prompt-templates/rule-ack/index.mjs +21 -21
  47. package/plugins/prompt-templates/storage.mjs +64 -64
  48. package/plugins/registry.mjs +16 -16
  49. package/web-ui/app.js +625 -634
  50. package/web-ui/index.html +35 -35
  51. package/web-ui/logic.agents-diff.mjs +386 -386
  52. package/web-ui/logic.claude.mjs +168 -168
  53. package/web-ui/logic.codex.mjs +56 -0
  54. package/web-ui/logic.mjs +5 -5
  55. package/web-ui/logic.runtime.mjs +128 -128
  56. package/web-ui/logic.session-convert.mjs +70 -70
  57. package/web-ui/logic.sessions.mjs +765 -765
  58. package/web-ui/modules/api.mjs +90 -90
  59. package/web-ui/modules/app.computed.dashboard.mjs +225 -171
  60. package/web-ui/modules/app.computed.index.mjs +17 -17
  61. package/web-ui/modules/app.computed.main-tabs.mjs +205 -205
  62. package/web-ui/modules/app.computed.session.mjs +994 -994
  63. package/web-ui/modules/app.constants.mjs +15 -15
  64. package/web-ui/modules/app.methods.agents.mjs +632 -632
  65. package/web-ui/modules/app.methods.claude-config.mjs +190 -184
  66. package/web-ui/modules/app.methods.codex-config.mjs +892 -860
  67. package/web-ui/modules/app.methods.index.mjs +92 -92
  68. package/web-ui/modules/app.methods.install.mjs +205 -205
  69. package/web-ui/modules/app.methods.navigation.mjs +743 -743
  70. package/web-ui/modules/app.methods.openclaw-core.mjs +814 -814
  71. package/web-ui/modules/app.methods.openclaw-editing.mjs +372 -372
  72. package/web-ui/modules/app.methods.openclaw-persist.mjs +369 -369
  73. package/web-ui/modules/app.methods.providers.mjs +412 -404
  74. package/web-ui/modules/app.methods.runtime.mjs +345 -345
  75. package/web-ui/modules/app.methods.session-actions.mjs +593 -596
  76. package/web-ui/modules/app.methods.session-browser.mjs +984 -989
  77. package/web-ui/modules/app.methods.session-timeline.mjs +479 -479
  78. package/web-ui/modules/app.methods.session-trash.mjs +439 -439
  79. package/web-ui/modules/app.methods.startup-claude.mjs +533 -526
  80. package/web-ui/modules/app.methods.task-orchestration.mjs +556 -556
  81. package/web-ui/modules/config-mode.computed.mjs +124 -124
  82. package/web-ui/modules/config-template-confirm-pref.mjs +33 -33
  83. package/web-ui/modules/i18n.dict.mjs +2109 -2131
  84. package/web-ui/modules/i18n.mjs +56 -56
  85. package/web-ui/modules/plugins.computed.mjs +3 -3
  86. package/web-ui/modules/plugins.methods.mjs +3 -3
  87. package/web-ui/modules/plugins.storage.mjs +11 -11
  88. package/web-ui/modules/provider-url-display.mjs +17 -0
  89. package/web-ui/modules/sessions-filters-url.mjs +85 -85
  90. package/web-ui/modules/skills.computed.mjs +107 -107
  91. package/web-ui/modules/skills.methods.mjs +481 -481
  92. package/web-ui/partials/index/layout-footer.html +13 -13
  93. package/web-ui/partials/index/layout-header.html +475 -475
  94. package/web-ui/partials/index/modal-config-template-agents.html +174 -174
  95. package/web-ui/partials/index/modal-confirm-toast.html +32 -32
  96. package/web-ui/partials/index/modal-health-check.html +45 -45
  97. package/web-ui/partials/index/modal-openclaw-config.html +280 -280
  98. package/web-ui/partials/index/modal-skills.html +200 -200
  99. package/web-ui/partials/index/modals-basic.html +165 -165
  100. package/web-ui/partials/index/panel-config-claude.html +188 -187
  101. package/web-ui/partials/index/panel-config-codex.html +312 -283
  102. package/web-ui/partials/index/panel-config-openclaw.html +83 -83
  103. package/web-ui/partials/index/panel-dashboard.html +186 -186
  104. package/web-ui/partials/index/panel-docs.html +147 -147
  105. package/web-ui/partials/index/panel-market.html +177 -177
  106. package/web-ui/partials/index/panel-orchestration.html +391 -391
  107. package/web-ui/partials/index/panel-plugins.html +253 -279
  108. package/web-ui/partials/index/panel-sessions.html +316 -326
  109. package/web-ui/partials/index/panel-settings.html +253 -274
  110. package/web-ui/partials/index/panel-usage.html +371 -371
  111. package/web-ui/res/json5.min.js +1 -1
  112. package/web-ui/res/vue.global.prod.js +13 -13
  113. package/web-ui/session-helpers.mjs +576 -576
  114. package/web-ui/source-bundle.cjs +233 -233
  115. package/web-ui/styles/base-theme.css +281 -268
  116. package/web-ui/styles/controls-forms.css +422 -423
  117. package/web-ui/styles/dashboard.css +274 -274
  118. package/web-ui/styles/docs-panel.css +247 -247
  119. package/web-ui/styles/feedback.css +108 -108
  120. package/web-ui/styles/health-check-dialog.css +144 -144
  121. package/web-ui/styles/layout-shell.css +606 -603
  122. package/web-ui/styles/modals-core.css +466 -464
  123. package/web-ui/styles/navigation-panels.css +391 -390
  124. package/web-ui/styles/openclaw-structured.css +266 -266
  125. package/web-ui/styles/plugins-panel.css +523 -523
  126. package/web-ui/styles/responsive.css +454 -454
  127. package/web-ui/styles/sessions-list.css +419 -415
  128. package/web-ui/styles/sessions-preview.css +411 -411
  129. package/web-ui/styles/sessions-toolbar-trash.css +330 -330
  130. package/web-ui/styles/sessions-usage.css +1040 -1040
  131. package/web-ui/styles/settings-panel.css +185 -185
  132. package/web-ui/styles/skills-list.css +303 -303
  133. package/web-ui/styles/skills-market.css +406 -406
  134. package/web-ui/styles/task-orchestration.css +822 -822
  135. package/web-ui/styles/titles-cards.css +472 -408
  136. package/web-ui/styles.css +21 -21
  137. package/web-ui.html +17 -17
@@ -1,1299 +1,1620 @@
1
- const http = require('http');
2
- const net = require('net');
3
- const crypto = require('crypto');
4
- const toml = require('@iarna/toml');
5
- const { readJsonFile, writeJsonAtomic } = require('../lib/cli-file-utils');
6
- const { isValidHttpUrl, normalizeBaseUrl, joinApiUrl } = require('../lib/cli-utils');
7
- const { toIsoTime } = require('../lib/cli-session-utils');
8
-
9
- function createBuiltinProxyRuntimeController(deps = {}) {
10
- const {
11
- fs,
12
- https,
13
- CONFIG_FILE,
14
- BUILTIN_PROXY_SETTINGS_FILE,
15
- DEFAULT_BUILTIN_PROXY_SETTINGS,
16
- BUILTIN_PROXY_PROVIDER_NAME,
17
- CODEXMATE_MANAGED_MARKER,
18
- HTTP_KEEP_ALIVE_AGENT,
19
- HTTPS_KEEP_ALIVE_AGENT,
20
- readConfig,
21
- writeConfig,
22
- readConfigOrVirtualDefault,
23
- resolveAuthTokenFromCurrentProfile,
24
- isPlainObject,
25
- isBuiltinManagedProvider,
26
- findProviderSectionRanges,
27
- findProviderDescendantSectionRanges,
28
- normalizeLegacySegments,
29
- buildLegacySegmentsKey,
30
- formatHostForUrl
31
- } = deps;
32
-
33
- if (!fs) throw new Error('createBuiltinProxyRuntimeController 缺少 fs');
34
- if (!https) throw new Error('createBuiltinProxyRuntimeController 缺少 https');
35
- if (!CONFIG_FILE) throw new Error('createBuiltinProxyRuntimeController 缺少 CONFIG_FILE');
36
- if (!BUILTIN_PROXY_SETTINGS_FILE) throw new Error('createBuiltinProxyRuntimeController 缺少 BUILTIN_PROXY_SETTINGS_FILE');
37
- if (!DEFAULT_BUILTIN_PROXY_SETTINGS || typeof DEFAULT_BUILTIN_PROXY_SETTINGS !== 'object') {
38
- throw new Error('createBuiltinProxyRuntimeController 缺少 DEFAULT_BUILTIN_PROXY_SETTINGS');
39
- }
40
- if (!BUILTIN_PROXY_PROVIDER_NAME) throw new Error('createBuiltinProxyRuntimeController 缺少 BUILTIN_PROXY_PROVIDER_NAME');
41
- if (typeof readConfig !== 'function') throw new Error('createBuiltinProxyRuntimeController 缺少 readConfig');
42
- if (typeof writeConfig !== 'function') throw new Error('createBuiltinProxyRuntimeController 缺少 writeConfig');
43
- if (typeof readConfigOrVirtualDefault !== 'function') {
44
- throw new Error('createBuiltinProxyRuntimeController 缺少 readConfigOrVirtualDefault');
45
- }
46
- if (typeof resolveAuthTokenFromCurrentProfile !== 'function') {
47
- throw new Error('createBuiltinProxyRuntimeController 缺少 resolveAuthTokenFromCurrentProfile');
48
- }
49
- if (typeof isPlainObject !== 'function') throw new Error('createBuiltinProxyRuntimeController 缺少 isPlainObject');
50
- if (typeof isBuiltinManagedProvider !== 'function') {
51
- throw new Error('createBuiltinProxyRuntimeController 缺少 isBuiltinManagedProvider');
52
- }
53
- if (typeof findProviderSectionRanges !== 'function') {
54
- throw new Error('createBuiltinProxyRuntimeController 缺少 findProviderSectionRanges');
55
- }
56
- if (typeof findProviderDescendantSectionRanges !== 'function') {
57
- throw new Error('createBuiltinProxyRuntimeController 缺少 findProviderDescendantSectionRanges');
58
- }
59
- if (typeof normalizeLegacySegments !== 'function') {
60
- throw new Error('createBuiltinProxyRuntimeController 缺少 normalizeLegacySegments');
61
- }
62
- if (typeof buildLegacySegmentsKey !== 'function') {
63
- throw new Error('createBuiltinProxyRuntimeController 缺少 buildLegacySegmentsKey');
64
- }
65
- if (typeof formatHostForUrl !== 'function') throw new Error('createBuiltinProxyRuntimeController 缺少 formatHostForUrl');
66
-
67
- let runtime = null;
68
-
69
- function readRequestBody(req, maxBytes) {
70
- return new Promise((resolve) => {
71
- let body = '';
72
- let size = 0;
73
- let aborted = false;
74
- req.on('data', (chunk) => {
75
- if (aborted) return;
76
- size += chunk.length;
77
- if (Number.isFinite(maxBytes) && maxBytes > 0 && size > maxBytes) {
78
- aborted = true;
79
- try { req.destroy(); } catch (_) {}
80
- resolve({ error: '请求体过大' });
81
- return;
82
- }
83
- body += chunk;
84
- });
85
- req.on('end', () => {
86
- if (aborted) return;
87
- resolve({ body });
88
- });
89
- req.on('error', (err) => resolve({ error: err && err.message ? err.message : 'request failed' }));
90
- });
91
- }
92
-
93
- function parseJsonOrError(text) {
94
- if (typeof text !== 'string' || !text.trim()) {
95
- return { value: null, error: 'empty body' };
96
- }
97
- try {
98
- return { value: JSON.parse(text), error: '' };
99
- } catch (e) {
100
- return { value: null, error: e && e.message ? e.message : 'invalid json' };
101
- }
102
- }
103
-
104
- function shouldFallbackFromUpstreamResponses(status, bodyText) {
105
- if (!Number.isFinite(status)) return false;
106
- if (status === 404 || status === 405 || status === 501) return true;
107
- const text = String(bodyText || '');
108
- if (!text) return false;
109
- if (/not implemented/i.test(text)) return true;
110
- if (/convert_request_failed/i.test(text)) return true;
111
- try {
112
- const parsed = JSON.parse(text);
113
- const code = parsed && parsed.error && typeof parsed.error.code === 'string' ? parsed.error.code : '';
114
- const msg = parsed && parsed.error && typeof parsed.error.message === 'string' ? parsed.error.message : '';
115
- if (code === 'convert_request_failed') return true;
116
- if (/not implemented/i.test(msg)) return true;
117
- } catch (_) {}
118
- return false;
119
- }
120
-
121
- function proxyRequestJson(targetUrl, options = {}) {
122
- const parsed = new URL(targetUrl);
123
- const transport = parsed.protocol === 'https:' ? https : http;
124
- const bodyText = options.body ? JSON.stringify(options.body) : '';
125
- const headers = {
126
- 'Accept': 'application/json',
127
- ...(options.body ? { 'Content-Type': 'application/json' } : {}),
128
- ...(options.headers || {})
129
- };
130
- if (options.body) {
131
- headers['Content-Length'] = Buffer.byteLength(bodyText, 'utf-8');
132
- }
133
- const timeoutMs = Number.isFinite(options.timeoutMs)
134
- ? Math.max(1000, Number(options.timeoutMs))
135
- : 30000;
136
-
137
- return new Promise((resolve) => {
138
- let settled = false;
139
- const finish = (value) => {
140
- if (settled) return;
141
- settled = true;
142
- resolve(value);
143
- };
144
- const req = transport.request({
145
- protocol: parsed.protocol,
146
- hostname: parsed.hostname,
147
- port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
148
- method: options.method || 'GET',
149
- path: `${parsed.pathname}${parsed.search}`,
150
- headers,
151
- agent: parsed.protocol === 'https:' ? HTTPS_KEEP_ALIVE_AGENT : HTTP_KEEP_ALIVE_AGENT
152
- }, (upstreamRes) => {
153
- const chunks = [];
154
- upstreamRes.on('data', (chunk) => chunk && chunks.push(chunk));
155
- upstreamRes.on('end', () => {
156
- const text = chunks.length ? Buffer.concat(chunks).toString('utf-8') : '';
157
- finish({
158
- ok: true,
159
- status: upstreamRes.statusCode || 0,
160
- headers: upstreamRes.headers || {},
161
- bodyText: text
162
- });
163
- });
164
- });
165
- req.setTimeout(timeoutMs, () => {
166
- try { req.destroy(new Error('timeout')); } catch (_) {}
167
- finish({ ok: false, error: 'timeout' });
168
- });
169
- req.on('error', (err) => finish({ ok: false, error: err && err.message ? err.message : 'request failed' }));
170
- if (bodyText) {
171
- req.write(bodyText);
172
- }
173
- req.end();
174
- });
175
- }
176
-
177
- function buildUpstreamUrlCandidates(baseUrl, pathSuffix) {
178
- const safeSuffix = String(pathSuffix || '').replace(/^\/+/, '');
179
- const candidates = [];
180
- const push = (url) => {
181
- if (url && !candidates.includes(url)) {
182
- candidates.push(url);
183
- }
184
- };
185
- push(joinApiUrl(baseUrl, safeSuffix));
186
- const trimmed = normalizeBaseUrl(baseUrl);
187
- if (trimmed && safeSuffix) {
188
- push(`${trimmed}/${safeSuffix}`);
189
- }
190
- return candidates;
191
- }
192
-
193
- async function proxyRequestJsonWithFallbackUrls(baseUrl, pathSuffix, options = {}) {
194
- const urls = buildUpstreamUrlCandidates(baseUrl, pathSuffix);
195
- if (urls.length === 0) {
196
- return { ok: false, error: 'failed to build upstream URL' };
197
- }
198
- let lastResult = null;
199
- for (let index = 0; index < urls.length; index += 1) {
200
- const result = await proxyRequestJson(urls[index], options);
201
- lastResult = result;
202
- if (!result.ok) {
203
- return result;
204
- }
205
- if (!(result.status === 404 || result.status === 405)) {
206
- return result;
207
- }
208
- }
209
- return lastResult || { ok: false, error: 'failed to build upstream URL' };
210
- }
211
-
212
- function stringifyJsonValue(value, fallback = '') {
213
- if (typeof value === 'string') return value;
214
- if (value == null) return fallback;
215
- try {
216
- return JSON.stringify(value);
217
- } catch (_) {
218
- return fallback;
219
- }
220
- }
221
-
222
- function normalizeChatUsageToResponsesUsage(usage) {
223
- if (!usage || typeof usage !== 'object' || Array.isArray(usage)) return undefined;
224
- const pickNumber = (...keys) => {
225
- for (const key of keys) {
226
- if (Number.isFinite(usage[key])) return usage[key];
227
- }
228
- return undefined;
229
- };
230
- const inputTokens = pickNumber('input_tokens', 'prompt_tokens');
231
- const outputTokens = pickNumber('output_tokens', 'completion_tokens');
232
- const totalTokens = pickNumber('total_tokens');
233
- const result = {};
234
- if (inputTokens != null) result.input_tokens = inputTokens;
235
- if (outputTokens != null) result.output_tokens = outputTokens;
236
- if (totalTokens != null) result.total_tokens = totalTokens;
237
- if (usage.input_tokens_details && typeof usage.input_tokens_details === 'object') {
238
- result.input_tokens_details = usage.input_tokens_details;
239
- } else if (usage.prompt_tokens_details && typeof usage.prompt_tokens_details === 'object') {
240
- result.input_tokens_details = usage.prompt_tokens_details;
241
- }
242
- if (usage.output_tokens_details && typeof usage.output_tokens_details === 'object') {
243
- result.output_tokens_details = usage.output_tokens_details;
244
- } else if (usage.completion_tokens_details && typeof usage.completion_tokens_details === 'object') {
245
- result.output_tokens_details = usage.completion_tokens_details;
246
- }
247
- return Object.keys(result).length > 0 ? result : usage;
248
- }
249
-
250
- function mapChatFinishReasonToResponses(choice) {
251
- const finishReason = choice && typeof choice === 'object' && typeof choice.finish_reason === 'string'
252
- ? choice.finish_reason
253
- : '';
254
- if (finishReason === 'length') {
255
- return { status: 'incomplete', incomplete_details: { reason: 'max_output_tokens' } };
256
- }
257
- if (finishReason === 'content_filter') {
258
- return { status: 'incomplete', incomplete_details: { reason: 'content_filter' } };
259
- }
260
- return { status: 'completed' };
261
- }
262
-
263
- function normalizeChatMessageContentToResponsesContent(content, refusal = '') {
264
- const blocks = [];
265
- const pushText = (text) => {
266
- if (typeof text === 'string' && text) {
267
- blocks.push({ type: 'output_text', text });
268
- }
269
- };
270
- if (typeof content === 'string') {
271
- pushText(content);
272
- } else if (Array.isArray(content)) {
273
- for (const item of content) {
274
- if (!item) continue;
275
- if (typeof item === 'string') {
276
- pushText(item);
277
- continue;
278
- }
279
- if (typeof item !== 'object') continue;
280
- const type = typeof item.type === 'string' ? item.type : '';
281
- if ((type === 'text' || type === 'output_text') && typeof item.text === 'string') {
282
- pushText(item.text);
283
- continue;
284
- }
285
- if (typeof item.content === 'string') {
286
- pushText(item.content);
287
- }
288
- }
289
- }
290
- if (typeof refusal === 'string' && refusal) {
291
- blocks.push({ type: 'refusal', refusal });
292
- }
293
- return blocks;
294
- }
295
-
296
- function buildResponsesPayloadFromChatCompletion(payload, fallbackModel = '') {
297
- const base = payload && typeof payload === 'object' && !Array.isArray(payload) ? payload : {};
298
- const choice = Array.isArray(base.choices) ? base.choices[0] : null;
299
- const message = choice && typeof choice === 'object' && choice.message && typeof choice.message === 'object'
300
- ? choice.message
301
- : {};
302
- const output = [];
303
- const messageContent = normalizeChatMessageContentToResponsesContent(message.content, message.refusal);
304
- if (messageContent.length > 0 || !Array.isArray(message.tool_calls) || message.tool_calls.length === 0) {
305
- output.push({
306
- type: 'message',
307
- role: 'assistant',
308
- content: messageContent.length > 0 ? messageContent : [{ type: 'output_text', text: '' }]
309
- });
310
- }
311
- if (Array.isArray(message.tool_calls)) {
312
- for (const toolCall of message.tool_calls) {
313
- if (!toolCall || typeof toolCall !== 'object') continue;
314
- const fn = toolCall.function && typeof toolCall.function === 'object' ? toolCall.function : {};
315
- const name = typeof fn.name === 'string' ? fn.name : '';
316
- if (!name) continue;
317
- output.push({
318
- type: 'function_call',
319
- call_id: typeof toolCall.id === 'string' && toolCall.id ? toolCall.id : `call_${crypto.randomBytes(8).toString('hex')}`,
320
- name,
321
- arguments: stringifyJsonValue(fn.arguments, '{}')
322
- });
323
- }
324
- }
325
- const finish = mapChatFinishReasonToResponses(choice);
326
- return ensureResponseMetadata({
327
- id: typeof base.id === 'string' ? base.id : undefined,
328
- model: typeof base.model === 'string' ? base.model : fallbackModel,
329
- status: finish.status,
330
- ...(finish.incomplete_details ? { incomplete_details: finish.incomplete_details } : {}),
331
- output,
332
- usage: normalizeChatUsageToResponsesUsage(base.usage)
333
- });
334
- }
335
-
336
- function normalizeResponsesInputToChatMessages(input) {
337
- // 参考 cc-switch Responses 转换形态:message content 保持为消息,function_call /
338
- // function_call_output 提升为 OpenAI Chat 的 assistant tool_calls / tool 消息。
339
- const toChatContent = (blocks) => {
340
- if (!Array.isArray(blocks)) return '';
341
- const out = [];
342
- for (const block of blocks) {
343
- if (!block || typeof block !== 'object') continue;
344
- const type = typeof block.type === 'string' ? block.type : '';
345
- if ((type === 'input_text' || type === 'output_text' || type === 'text') && typeof block.text === 'string') {
346
- out.push({ type: 'text', text: block.text });
347
- continue;
348
- }
349
- if (type === 'refusal' && typeof block.refusal === 'string') {
350
- out.push({ type: 'text', text: block.refusal });
351
- continue;
352
- }
353
- if (type === 'input_image') {
354
- const raw = block.image_url != null ? block.image_url : block.imageUrl;
355
- const url = typeof raw === 'string'
356
- ? raw
357
- : (raw && typeof raw === 'object' && typeof raw.url === 'string' ? raw.url : '');
358
- if (url) {
359
- out.push({ type: 'image_url', image_url: { url } });
360
- }
361
- continue;
362
- }
363
- if (type === 'image_url' && block.image_url) {
364
- out.push({ type: 'image_url', image_url: block.image_url });
365
- }
366
- }
367
- if (out.length === 0) return '';
368
- return out;
369
- };
370
-
371
- const messageFromResponsesItem = (item) => {
372
- if (!item || typeof item !== 'object') return null;
373
- const type = typeof item.type === 'string' ? item.type : '';
374
- if (type === 'function_call') {
375
- const name = typeof item.name === 'string' ? item.name : '';
376
- if (!name) return null;
377
- return {
378
- role: 'assistant',
379
- content: null,
380
- tool_calls: [{
381
- id: typeof item.call_id === 'string' && item.call_id ? item.call_id : (typeof item.id === 'string' ? item.id : `call_${crypto.randomBytes(8).toString('hex')}`),
382
- type: 'function',
383
- function: {
384
- name,
385
- arguments: stringifyJsonValue(item.arguments, '{}')
386
- }
387
- }]
388
- };
389
- }
390
- if (type === 'function_call_output') {
391
- const callId = typeof item.call_id === 'string' ? item.call_id : '';
392
- return {
393
- role: 'tool',
394
- tool_call_id: callId,
395
- content: stringifyJsonValue(item.output, '')
396
- };
397
- }
398
- if (typeof item.role === 'string' && item.content != null) {
399
- const role = item.role.trim() || 'user';
400
- const content = Array.isArray(item.content)
401
- ? toChatContent(item.content)
402
- : item.content;
403
- return content || content === null ? { role, content } : null;
404
- }
405
- if (type) {
406
- const content = toChatContent([item]);
407
- return content ? { role: 'user', content } : null;
408
- }
409
- return null;
410
- };
411
-
412
- if (typeof input === 'string') {
413
- return [{ role: 'user', content: input }];
414
- }
415
- if (input && typeof input === 'object' && !Array.isArray(input)) {
416
- const message = messageFromResponsesItem(input);
417
- return message ? [message] : [];
418
- }
419
- if (!Array.isArray(input)) {
420
- return [];
421
- }
422
-
423
- const messages = [];
424
- for (const item of input) {
425
- const message = messageFromResponsesItem(item);
426
- if (message) messages.push(message);
427
- }
428
- if (messages.length > 0) {
429
- return messages;
430
- }
431
-
432
- const fallbackContent = toChatContent(input);
433
- if (fallbackContent) {
434
- return [{ role: 'user', content: fallbackContent }];
435
- }
436
- return [];
437
- }
438
-
439
- function normalizeResponsesToolsToChatTools(tools) {
440
- if (!Array.isArray(tools)) return tools;
441
- return tools
442
- .map((tool) => {
443
- if (!tool || typeof tool !== 'object') return null;
444
- if (tool.type !== 'function') return tool;
445
- const sourceFn = tool.function && typeof tool.function === 'object' && !Array.isArray(tool.function)
446
- ? tool.function
447
- : {};
448
- const name = typeof sourceFn.name === 'string' && sourceFn.name.trim()
449
- ? sourceFn.name.trim()
450
- : (typeof tool.name === 'string' ? tool.name.trim() : '');
451
- if (!name) return null;
452
- const description = typeof sourceFn.description === 'string'
453
- ? sourceFn.description
454
- : (typeof tool.description === 'string' ? tool.description : undefined);
455
- const parameters = sourceFn.parameters && typeof sourceFn.parameters === 'object' && !Array.isArray(sourceFn.parameters)
456
- ? sourceFn.parameters
457
- : (tool.parameters && typeof tool.parameters === 'object' && !Array.isArray(tool.parameters) ? tool.parameters : {});
458
- const strict = typeof sourceFn.strict === 'boolean'
459
- ? sourceFn.strict
460
- : (typeof tool.strict === 'boolean' ? tool.strict : undefined);
461
- const fn = { name, parameters };
462
- if (description !== undefined) fn.description = description;
463
- if (strict !== undefined) fn.strict = strict;
464
- return { type: 'function', function: fn };
465
- })
466
- .filter(Boolean);
467
- }
468
-
469
- function normalizeResponsesToolChoiceToChatToolChoice(toolChoice) {
470
- if (!toolChoice || typeof toolChoice !== 'object' || Array.isArray(toolChoice)) return toolChoice;
471
- if (toolChoice.type === 'function' && typeof toolChoice.name === 'string') {
472
- return { type: 'function', function: { name: toolChoice.name } };
473
- }
474
- return toolChoice;
475
- }
476
-
477
- function buildChatCompletionsBodyFromResponsesPayload(payload) {
478
- const source = payload && typeof payload === 'object' && !Array.isArray(payload) ? payload : {};
479
- const messages = normalizeResponsesInputToChatMessages(source.input);
480
- const instructions = typeof source.instructions === 'string' ? source.instructions.trim() : '';
481
- if (instructions) {
482
- messages.unshift({ role: 'system', content: instructions });
483
- }
484
-
485
- const chatBody = {
486
- model: typeof source.model === 'string' ? source.model : '',
487
- messages,
488
- stream: false
489
- };
490
-
491
- const passthroughKeys = [
492
- 'frequency_penalty',
493
- 'presence_penalty',
494
- 'response_format',
495
- 'stop',
496
- 'temperature',
497
- 'top_p',
498
- 'tools',
499
- 'tool_choice',
500
- 'logprobs',
501
- 'top_logprobs',
502
- 'kbs',
503
- 'is_online',
504
- 'user',
505
- 'seed',
506
- 'n',
507
- 'modalities',
508
- 'audio',
509
- 'reasoning_effort'
510
- ];
511
- for (const key of passthroughKeys) {
512
- if (Object.prototype.hasOwnProperty.call(source, key)) {
513
- if (key === 'tools') {
514
- chatBody[key] = normalizeResponsesToolsToChatTools(source[key]);
515
- } else if (key === 'tool_choice') {
516
- chatBody[key] = normalizeResponsesToolChoiceToChatToolChoice(source[key]);
517
- } else {
518
- chatBody[key] = source[key];
519
- }
520
- }
521
- }
522
-
523
- if (Object.prototype.hasOwnProperty.call(source, 'max_tokens')) {
524
- chatBody.max_tokens = source.max_tokens;
525
- } else if (source.max_output_tokens != null) {
526
- chatBody.max_tokens = source.max_output_tokens;
527
- }
528
-
529
- return chatBody;
530
- }
531
-
532
- function ensureResponseMetadata(payload) {
533
- const base = payload && typeof payload === 'object' && !Array.isArray(payload) ? payload : {};
534
- const id = typeof base.id === 'string' && base.id.trim()
535
- ? base.id.trim()
536
- : `resp_${crypto.randomBytes(10).toString('hex')}`;
537
- const model = typeof base.model === 'string' ? base.model : '';
538
- return {
539
- object: 'response',
540
- id,
541
- model,
542
- ...base
543
- };
544
- }
545
-
546
- function writeSse(res, eventName, dataObj) {
547
- if (eventName) {
548
- res.write(`event: ${eventName}\n`);
549
- }
550
- if (dataObj === '[DONE]') {
551
- res.write('data: [DONE]\n\n');
552
- return;
553
- }
554
- res.write(`data: ${JSON.stringify(dataObj)}\n\n`);
555
- }
556
-
557
- function sendResponsesSse(res, responsePayload) {
558
- const response = ensureResponseMetadata(responsePayload);
559
- const responseId = response.id;
560
- const model = response.model;
561
- let sequence = 0;
562
- const nextSeq = () => {
563
- sequence += 1;
564
- return sequence;
565
- };
566
-
567
- writeSse(res, 'response.created', {
568
- type: 'response.created',
569
- response: {
570
- id: responseId,
571
- model,
572
- created_at: response.created_at
573
- }
574
- });
575
-
576
- const output = Array.isArray(response.output) ? response.output : [];
577
- for (let outputIndex = 0; outputIndex < output.length; outputIndex += 1) {
578
- const item = output[outputIndex];
579
- if (!item || typeof item !== 'object') continue;
580
- const itemType = typeof item.type === 'string' ? item.type : '';
581
- const itemId = typeof item.id === 'string' && item.id.trim()
582
- ? item.id.trim()
583
- : `item_${crypto.randomBytes(8).toString('hex')}`;
584
-
585
- writeSse(res, 'response.output_item.added', {
586
- type: 'response.output_item.added',
587
- output_index: outputIndex,
588
- item: { ...item, id: itemId }
589
- });
590
-
591
- if (itemType === 'message') {
592
- const content = Array.isArray(item.content) ? item.content : [];
593
- for (let contentIndex = 0; contentIndex < content.length; contentIndex += 1) {
594
- const block = content[contentIndex];
595
- if (!block || typeof block !== 'object') continue;
596
- if (block.type !== 'output_text') continue;
597
- const text = typeof block.text === 'string' ? block.text : '';
598
- if (text) {
599
- writeSse(res, 'response.output_text.delta', {
600
- type: 'response.output_text.delta',
601
- item_id: itemId,
602
- output_index: outputIndex,
603
- content_index: contentIndex,
604
- delta: text,
605
- sequence_number: nextSeq()
606
- });
607
- }
608
- writeSse(res, 'response.output_text.done', {
609
- type: 'response.output_text.done',
610
- item_id: itemId,
611
- output_index: outputIndex,
612
- content_index: contentIndex,
613
- text,
614
- sequence_number: nextSeq()
615
- });
616
- }
617
- }
618
-
619
- writeSse(res, 'response.output_item.done', {
620
- type: 'response.output_item.done',
621
- output_index: outputIndex,
622
- item: { ...item, id: itemId },
623
- sequence_number: nextSeq()
624
- });
625
- }
626
-
627
- writeSse(res, 'response.completed', { type: 'response.completed', response });
628
- writeSse(res, 'done', '[DONE]');
629
- }
630
-
631
- function canListenPort(host, port) {
632
- return new Promise((resolve) => {
633
- const tester = net.createServer();
634
- tester.unref();
635
- tester.once('error', () => resolve(false));
636
- tester.once('listening', () => {
637
- tester.close(() => resolve(true));
638
- });
639
- tester.listen(port, host);
640
- });
641
- }
642
-
643
- async function findAvailablePort(host, startPort, maxAttempts = 20) {
644
- const start = parseInt(String(startPort), 10);
645
- if (!Number.isFinite(start) || start <= 0) {
646
- return 0;
647
- }
648
- const attempts = Number.isFinite(maxAttempts) && maxAttempts > 0 ? maxAttempts : 20;
649
- for (let offset = 0; offset < attempts; offset += 1) {
650
- const candidate = start + offset;
651
- if (candidate > 65535) {
652
- break;
653
- }
654
- // eslint-disable-next-line no-await-in-loop
655
- const ok = await canListenPort(host, candidate);
656
- if (ok) {
657
- return candidate;
658
- }
659
- }
660
- return 0;
661
- }
662
-
663
- function resolveBuiltinProxyProviderName(rawProviderName, providers = {}, preferredProvider = '') {
664
- const providerMap = providers && isPlainObject(providers) ? providers : {};
665
- const providerNames = Object.keys(providerMap)
666
- .filter((name) => name && !isBuiltinManagedProvider(name));
667
- const requested = typeof rawProviderName === 'string' ? rawProviderName.trim() : '';
668
- if (requested && !isBuiltinManagedProvider(requested) && providerMap[requested]) {
669
- return requested;
670
- }
671
- const preferred = typeof preferredProvider === 'string' ? preferredProvider.trim() : '';
672
- if (preferred && !isBuiltinManagedProvider(preferred) && providerMap[preferred]) {
673
- return preferred;
674
- }
675
- return providerNames[0] || '';
676
- }
677
-
678
- function normalizeBuiltinProxySettings(raw) {
679
- const merged = {
680
- ...DEFAULT_BUILTIN_PROXY_SETTINGS,
681
- ...(isPlainObject(raw) ? raw : {})
682
- };
683
- const host = typeof merged.host === 'string' ? merged.host.trim() : '';
684
- const port = parseInt(String(merged.port), 10);
685
- const provider = typeof merged.provider === 'string' ? merged.provider.trim() : '';
686
- const authSourceRaw = typeof merged.authSource === 'string' ? merged.authSource.trim().toLowerCase() : '';
687
- const timeoutMs = parseInt(String(merged.timeoutMs), 10);
688
- const authSource = authSourceRaw === 'profile' || authSourceRaw === 'none' ? authSourceRaw : 'provider';
689
-
690
- return {
691
- enabled: merged.enabled !== false,
692
- host: host || DEFAULT_BUILTIN_PROXY_SETTINGS.host,
693
- port: Number.isFinite(port) && port > 0 && port <= 65535 ? port : DEFAULT_BUILTIN_PROXY_SETTINGS.port,
694
- provider,
695
- authSource,
696
- timeoutMs: Number.isFinite(timeoutMs) && timeoutMs >= 1000
697
- ? timeoutMs
698
- : DEFAULT_BUILTIN_PROXY_SETTINGS.timeoutMs
699
- };
700
- }
701
-
702
- function readBuiltinProxySettings() {
703
- const parsed = readJsonFile(BUILTIN_PROXY_SETTINGS_FILE, null);
704
- return normalizeBuiltinProxySettings(parsed);
705
- }
706
-
707
- function saveBuiltinProxySettings(payload = {}, options = {}) {
708
- const current = readBuiltinProxySettings();
709
- const merged = normalizeBuiltinProxySettings({
710
- ...current,
711
- ...(isPlainObject(payload) ? payload : {})
712
- });
713
-
714
- if (!merged.host) {
715
- return { error: '代理 host 不能为空' };
716
- }
717
- if (!Number.isFinite(merged.port) || merged.port <= 0 || merged.port > 65535) {
718
- return { error: '代理端口无效(1-65535)' };
719
- }
720
-
721
- const { config } = readConfigOrVirtualDefault();
722
- const providers = config && isPlainObject(config.model_providers) ? config.model_providers : {};
723
- const preferredProvider = typeof config.model_provider === 'string' ? config.model_provider.trim() : '';
724
- const finalProvider = resolveBuiltinProxyProviderName(merged.provider, providers, preferredProvider);
725
-
726
- const normalized = {
727
- ...merged,
728
- provider: finalProvider
729
- };
730
-
731
- if (!options.skipWrite) {
732
- writeJsonAtomic(BUILTIN_PROXY_SETTINGS_FILE, normalized);
733
- }
734
-
735
- return {
736
- success: true,
737
- settings: normalized
738
- };
739
- }
740
-
741
- function buildProxyListenUrl(settings) {
742
- const host = formatHostForUrl(settings.host || DEFAULT_BUILTIN_PROXY_SETTINGS.host);
743
- return `http://${host}:${settings.port}`;
744
- }
745
-
746
- function buildBuiltinProxyProviderBaseUrl(settings) {
747
- return `${buildProxyListenUrl(settings).replace(/\/+$/, '')}/v1`;
748
- }
749
-
750
- function removePersistedBuiltinProxyProviderFromConfig() {
751
- if (!fs.existsSync(CONFIG_FILE)) {
752
- return { success: true, removed: false };
753
- }
754
-
755
- let config;
756
- try {
757
- config = readConfig();
758
- } catch (e) {
759
- return { error: e.message || '读取 config.toml 失败' };
760
- }
761
-
762
- if (!config.model_providers || !config.model_providers[BUILTIN_PROXY_PROVIDER_NAME]) {
763
- return { success: true, removed: false };
764
- }
765
-
766
- const content = fs.readFileSync(CONFIG_FILE, 'utf-8');
767
- const lineEnding = content.includes('\r\n') ? '\r\n' : '\n';
768
- const hasBom = content.charCodeAt(0) === 0xFEFF;
769
- const providerConfig = config.model_providers[BUILTIN_PROXY_PROVIDER_NAME];
770
- const providerSegments = providerConfig && Array.isArray(providerConfig.__codexmate_legacy_segments)
771
- ? providerConfig.__codexmate_legacy_segments
772
- : null;
773
- const providerSegmentVariants = (() => {
774
- const variants = [];
775
- const seen = new Set();
776
- const pushVariant = (segments) => {
777
- const normalized = normalizeLegacySegments(segments);
778
- const key = buildLegacySegmentsKey(normalized);
779
- if (!key || seen.has(key)) return;
780
- seen.add(key);
781
- variants.push(normalized);
782
- };
783
- if (providerConfig && Array.isArray(providerConfig.__codexmate_legacy_segments)) {
784
- pushVariant(providerConfig.__codexmate_legacy_segments);
785
- }
786
- if (providerConfig && Array.isArray(providerConfig.__codexmate_legacy_segment_variants)) {
787
- for (const segments of providerConfig.__codexmate_legacy_segment_variants) {
788
- pushVariant(segments);
789
- }
790
- }
791
- if (providerSegments) {
792
- pushVariant(providerSegments);
793
- }
794
- if (variants.length === 0) {
795
- pushVariant(String(BUILTIN_PROXY_PROVIDER_NAME || '').split('.').filter((item) => item));
796
- }
797
- return variants;
798
- })();
799
-
800
- let updatedContent = null;
801
- const combinedRanges = [];
802
- for (const segments of providerSegmentVariants) {
803
- combinedRanges.push(...findProviderSectionRanges(content, BUILTIN_PROXY_PROVIDER_NAME, segments));
804
- combinedRanges.push(...findProviderDescendantSectionRanges(content, segments));
805
- }
806
- if (combinedRanges.length === 0) {
807
- combinedRanges.push(...findProviderSectionRanges(content, BUILTIN_PROXY_PROVIDER_NAME, providerSegments));
808
- }
809
-
810
- if (combinedRanges.length > 0) {
811
- const sorted = combinedRanges.sort((a, b) => b.start - a.start || b.end - a.end);
812
- const seen = new Set();
813
- let removedContent = content;
814
- for (const range of sorted) {
815
- const rangeKey = `${range.start}:${range.end}`;
816
- if (seen.has(rangeKey)) continue;
817
- seen.add(rangeKey);
818
- removedContent = removedContent.slice(0, range.start) + removedContent.slice(range.end);
819
- }
820
- updatedContent = removedContent.replace(/\n{3,}/g, lineEnding + lineEnding);
821
- }
822
-
823
- if (!updatedContent) {
824
- const rebuilt = JSON.parse(JSON.stringify(config));
825
- delete rebuilt.model_providers[BUILTIN_PROXY_PROVIDER_NAME];
826
- const hasMarker = content.includes(CODEXMATE_MANAGED_MARKER);
827
- let rebuiltToml = toml.stringify(rebuilt).trimEnd();
828
- rebuiltToml = rebuiltToml.replace(/\n/g, lineEnding);
829
- if (hasMarker && !rebuiltToml.includes(CODEXMATE_MANAGED_MARKER)) {
830
- rebuiltToml = `${CODEXMATE_MANAGED_MARKER}${lineEnding}${rebuiltToml}`;
831
- }
832
- updatedContent = rebuiltToml + lineEnding;
833
- if (hasBom && updatedContent.charCodeAt(0) !== 0xFEFF) {
834
- updatedContent = '\uFEFF' + updatedContent;
835
- }
836
- }
837
-
838
- try {
839
- writeConfig(updatedContent.trimEnd() + lineEnding);
840
- } catch (e) {
841
- return { error: e.message || '写入 config.toml 失败' };
842
- }
843
-
844
- return { success: true, removed: true };
845
- }
846
-
847
- function hasCodexConfigReadyForProxy() {
848
- const result = readConfigOrVirtualDefault();
849
- if (!result || result.isVirtual) {
850
- return false;
851
- }
852
- const config = result.config || {};
853
- if (!isPlainObject(config.model_providers)) {
854
- return false;
855
- }
856
- const providerNames = Object.keys(config.model_providers)
857
- .filter((name) => name && !isBuiltinManagedProvider(name));
858
- return providerNames.length > 0;
859
- }
860
-
861
- function resolveBuiltinProxyUpstream(settings) {
862
- const { config } = readConfigOrVirtualDefault();
863
- const providers = config && isPlainObject(config.model_providers) ? config.model_providers : {};
864
- const currentProvider = typeof config.model_provider === 'string' ? config.model_provider.trim() : '';
865
- const providerName = resolveBuiltinProxyProviderName(settings.provider, providers, currentProvider);
866
- if (!providerName) {
867
- return { error: '未找到可用的上游 provider,请先添加 provider' };
868
- }
869
- if (providerName === BUILTIN_PROXY_PROVIDER_NAME) {
870
- return { error: `上游 provider 不能是 ${BUILTIN_PROXY_PROVIDER_NAME}` };
871
- }
872
- const provider = providers[providerName];
873
- if (!provider || !isPlainObject(provider)) {
874
- return { error: `上游 provider 不存在: ${providerName}` };
875
- }
876
-
877
- const baseUrl = typeof provider.base_url === 'string' ? provider.base_url.trim() : '';
878
- if (!baseUrl || !isValidHttpUrl(baseUrl)) {
879
- return { error: `上游 provider base_url 无效: ${providerName}` };
880
- }
881
-
882
- let token = '';
883
- if (settings.authSource === 'profile') {
884
- token = resolveAuthTokenFromCurrentProfile();
885
- } else if (settings.authSource === 'provider') {
886
- token = typeof provider.preferred_auth_method === 'string' ? provider.preferred_auth_method.trim() : '';
887
- if (!token) {
888
- token = resolveAuthTokenFromCurrentProfile();
889
- }
890
- }
891
-
892
- let authHeader = '';
893
- if (token) {
894
- authHeader = /^bearer\s+/i.test(token) ? token : `Bearer ${token}`;
895
- }
896
-
897
- return {
898
- providerName,
899
- baseUrl: normalizeBaseUrl(baseUrl),
900
- authHeader
901
- };
902
- }
903
-
904
- function createBuiltinProxyServer(settings, upstream) {
905
- const connections = new Set();
906
- const timeoutMs = settings.timeoutMs;
907
-
908
- const server = http.createServer((req, res) => {
909
- let parsedIncoming;
910
- try {
911
- parsedIncoming = new URL(req.url || '/', 'http://localhost');
912
- } catch (e) {
913
- res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
914
- res.end(JSON.stringify({ error: 'invalid request path' }));
915
- return;
916
- }
917
-
918
- const remoteAddr = req && req.socket ? req.socket.remoteAddress : '';
919
- const isLoopback = !remoteAddr
920
- || remoteAddr === '127.0.0.1'
921
- || remoteAddr === '::1'
922
- || remoteAddr === '::ffff:127.0.0.1';
923
- if (!isLoopback) {
924
- const expected = typeof process.env.CODEXMATE_HTTP_TOKEN === 'string'
925
- ? process.env.CODEXMATE_HTTP_TOKEN.trim()
926
- : '';
927
- if (!expected) {
928
- const body = JSON.stringify({ error: 'Remote access is disabled (set CODEXMATE_HTTP_TOKEN)' });
929
- res.writeHead(403, {
930
- 'Content-Type': 'application/json; charset=utf-8',
931
- 'Content-Length': Buffer.byteLength(body, 'utf-8')
932
- });
933
- res.end(body, 'utf-8');
934
- return;
935
- }
936
- const headers = req && req.headers && typeof req.headers === 'object' ? req.headers : {};
937
- const rawAuth = typeof headers.authorization === 'string' ? headers.authorization.trim() : '';
938
- const match = rawAuth ? rawAuth.match(/^bearer\s+(.+)$/i) : null;
939
- const actual = match && match[1]
940
- ? match[1].trim()
941
- : (rawAuth ? rawAuth : (typeof headers['x-codexmate-token'] === 'string' ? String(headers['x-codexmate-token']).trim() : ''));
942
- if (!actual || actual !== expected) {
943
- const body = JSON.stringify({ error: 'Unauthorized' });
944
- res.writeHead(401, {
945
- 'Content-Type': 'application/json; charset=utf-8',
946
- 'Content-Length': Buffer.byteLength(body, 'utf-8')
947
- });
948
- res.end(body, 'utf-8');
949
- return;
950
- }
951
- }
952
-
953
- const incomingPath = parsedIncoming.pathname || '/';
954
- if (incomingPath === '/health' || incomingPath === '/status') {
955
- const body = JSON.stringify({
956
- ok: true,
957
- upstreamProvider: upstream.providerName,
958
- upstreamBaseUrl: upstream.baseUrl
959
- });
960
- res.writeHead(200, {
961
- 'Content-Type': 'application/json; charset=utf-8',
962
- 'Content-Length': Buffer.byteLength(body, 'utf-8')
963
- });
964
- res.end(body, 'utf-8');
965
- return;
966
- }
967
-
968
- if (!(incomingPath === '/v1' || incomingPath.startsWith('/v1/'))) {
969
- const body = JSON.stringify({ error: 'proxy only supports /v1/* paths' });
970
- res.writeHead(404, {
971
- 'Content-Type': 'application/json; charset=utf-8',
972
- 'Content-Length': Buffer.byteLength(body, 'utf-8')
973
- });
974
- res.end(body, 'utf-8');
975
- return;
976
- }
977
-
978
- // Responses shim:
979
- // - Codex CLI 默认走 /v1/responses(含 SSE)
980
- // - 某些上游只支持 /v1/chat/completions
981
- // 因此这里优先尝试 /v1/responses(stream=false),失败再转换到 chat/completions 并回包为 responses。
982
- if ((incomingPath === '/v1/responses' || incomingPath === '/v1/responses/') && (req.method || 'GET').toUpperCase() === 'POST') {
983
- void (async () => {
984
- const { body, error } = await readRequestBody(req, 10 * 1024 * 1024);
985
- if (error) {
986
- res.writeHead(413, { 'Content-Type': 'application/json; charset=utf-8' });
987
- res.end(JSON.stringify({ error }));
988
- return;
989
- }
990
- const parsed = parseJsonOrError(body);
991
- if (parsed.error) {
992
- res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
993
- res.end(JSON.stringify({ error: `invalid json: ${parsed.error}` }));
994
- return;
995
- }
996
-
997
- const payload = parsed.value && typeof parsed.value === 'object' ? parsed.value : {};
998
- const wantsStream = payload.stream === true;
999
-
1000
- const commonHeaders = {
1001
- ...(upstream.authHeader ? { 'Authorization': upstream.authHeader } : {}),
1002
- 'X-Codexmate-Proxy': '1'
1003
- };
1004
-
1005
- const upstreamResponses = await proxyRequestJsonWithFallbackUrls(upstream.baseUrl, 'responses', {
1006
- method: 'POST',
1007
- headers: commonHeaders,
1008
- timeoutMs,
1009
- body: { ...payload, stream: false }
1010
- });
1011
-
1012
- // 优先走上游 /responses(如果支持)。若上游报错且不是“端点不支持”,则直接透传错误。
1013
- if (upstreamResponses.ok && upstreamResponses.status >= 200 && upstreamResponses.status < 300) {
1014
- const json = parseJsonOrError(upstreamResponses.bodyText);
1015
- if (json.error) {
1016
- res.writeHead(502, { 'Content-Type': 'application/json; charset=utf-8' });
1017
- res.end(JSON.stringify({ error: `Upstream JSON parse failed: ${json.error}` }));
1018
- return;
1019
- }
1020
- const responsesPayload = ensureResponseMetadata(json.value);
1021
- if (wantsStream) {
1022
- res.writeHead(200, {
1023
- 'Content-Type': 'text/event-stream; charset=utf-8',
1024
- 'Cache-Control': 'no-cache',
1025
- 'Connection': 'keep-alive',
1026
- 'X-Accel-Buffering': 'no'
1027
- });
1028
- sendResponsesSse(res, responsesPayload);
1029
- res.end();
1030
- return;
1031
- }
1032
- res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
1033
- res.end(JSON.stringify(responsesPayload));
1034
- return;
1035
- }
1036
-
1037
- if (upstreamResponses.ok && upstreamResponses.status >= 400) {
1038
- if (!shouldFallbackFromUpstreamResponses(upstreamResponses.status, upstreamResponses.bodyText)) {
1039
- res.writeHead(upstreamResponses.status, { 'Content-Type': 'application/json; charset=utf-8' });
1040
- res.end(upstreamResponses.bodyText || JSON.stringify({ error: 'Upstream error' }));
1041
- return;
1042
- }
1043
- // fallthrough to chat/completions conversion
1044
- }
1045
-
1046
- if (!upstreamResponses.ok) {
1047
- res.writeHead(502, { 'Content-Type': 'application/json; charset=utf-8' });
1048
- res.end(JSON.stringify({ error: upstreamResponses.error || 'Upstream request failed' }));
1049
- return;
1050
- }
1051
-
1052
- const model = typeof payload.model === 'string' ? payload.model : '';
1053
- const chatBody = buildChatCompletionsBodyFromResponsesPayload(payload);
1054
-
1055
- const upstreamChat = await proxyRequestJsonWithFallbackUrls(upstream.baseUrl, 'chat/completions', {
1056
- method: 'POST',
1057
- headers: commonHeaders,
1058
- timeoutMs,
1059
- body: chatBody
1060
- });
1061
- if (!upstreamChat.ok) {
1062
- res.writeHead(502, { 'Content-Type': 'application/json; charset=utf-8' });
1063
- res.end(JSON.stringify({ error: upstreamChat.error || 'proxy request failed' }));
1064
- return;
1065
- }
1066
-
1067
- if (upstreamChat.status >= 400) {
1068
- res.writeHead(upstreamChat.status, { 'Content-Type': 'application/json; charset=utf-8' });
1069
- res.end(upstreamChat.bodyText || JSON.stringify({ error: 'Upstream error' }));
1070
- return;
1071
- }
1072
-
1073
- const chatJson = parseJsonOrError(upstreamChat.bodyText);
1074
- if (chatJson.error) {
1075
- res.writeHead(502, { 'Content-Type': 'application/json; charset=utf-8' });
1076
- res.end(JSON.stringify({ error: `invalid upstream response: ${chatJson.error}` }));
1077
- return;
1078
- }
1079
-
1080
- const responsesPayload = buildResponsesPayloadFromChatCompletion(chatJson.value, model);
1081
-
1082
- if (wantsStream) {
1083
- res.writeHead(200, {
1084
- 'Content-Type': 'text/event-stream; charset=utf-8',
1085
- 'Cache-Control': 'no-cache',
1086
- 'Connection': 'keep-alive',
1087
- 'X-Accel-Buffering': 'no'
1088
- });
1089
- sendResponsesSse(res, responsesPayload);
1090
- res.end();
1091
- return;
1092
- }
1093
- res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
1094
- res.end(JSON.stringify(responsesPayload));
1095
- })();
1096
- return;
1097
- }
1098
-
1099
- const suffix = incomingPath === '/v1'
1100
- ? ''
1101
- : incomingPath.replace(/^\/v1\/?/, '');
1102
- const targetBase = joinApiUrl(upstream.baseUrl, suffix);
1103
- if (!targetBase) {
1104
- const body = JSON.stringify({ error: 'failed to build upstream URL' });
1105
- res.writeHead(500, {
1106
- 'Content-Type': 'application/json; charset=utf-8',
1107
- 'Content-Length': Buffer.byteLength(body, 'utf-8')
1108
- });
1109
- res.end(body, 'utf-8');
1110
- return;
1111
- }
1112
-
1113
- let targetUrl;
1114
- try {
1115
- targetUrl = new URL(targetBase);
1116
- targetUrl.search = parsedIncoming.search || '';
1117
- } catch (e) {
1118
- const body = JSON.stringify({ error: `invalid upstream URL: ${e.message}` });
1119
- res.writeHead(500, {
1120
- 'Content-Type': 'application/json; charset=utf-8',
1121
- 'Content-Length': Buffer.byteLength(body, 'utf-8')
1122
- });
1123
- res.end(body, 'utf-8');
1124
- return;
1125
- }
1126
-
1127
- const requestHeaders = { ...req.headers };
1128
- delete requestHeaders.host;
1129
- delete requestHeaders.connection;
1130
- delete requestHeaders['content-length'];
1131
- if (upstream.authHeader) {
1132
- requestHeaders.authorization = upstream.authHeader;
1133
- }
1134
- requestHeaders['x-codexmate-proxy'] = '1';
1135
- if (!requestHeaders['x-forwarded-for'] && req.socket && req.socket.remoteAddress) {
1136
- requestHeaders['x-forwarded-for'] = req.socket.remoteAddress;
1137
- }
1138
-
1139
- const transport = targetUrl.protocol === 'https:' ? https : http;
1140
- const upstreamReq = transport.request({
1141
- protocol: targetUrl.protocol,
1142
- hostname: targetUrl.hostname,
1143
- port: targetUrl.port || (targetUrl.protocol === 'https:' ? 443 : 80),
1144
- method: req.method || 'GET',
1145
- path: `${targetUrl.pathname}${targetUrl.search}`,
1146
- headers: requestHeaders,
1147
- agent: targetUrl.protocol === 'https:' ? HTTPS_KEEP_ALIVE_AGENT : HTTP_KEEP_ALIVE_AGENT
1148
- }, (upstreamRes) => {
1149
- const responseHeaders = { ...upstreamRes.headers };
1150
- delete responseHeaders.connection;
1151
- res.writeHead(upstreamRes.statusCode || 502, responseHeaders);
1152
- upstreamRes.pipe(res);
1153
- });
1154
-
1155
- upstreamReq.setTimeout(timeoutMs, () => {
1156
- upstreamReq.destroy(new Error(`upstream timeout (${timeoutMs}ms)`));
1157
- });
1158
-
1159
- upstreamReq.on('error', (err) => {
1160
- if (res.headersSent) {
1161
- try { res.destroy(err); } catch (_) {}
1162
- return;
1163
- }
1164
- const body = JSON.stringify({ error: `proxy request failed: ${err.message}` });
1165
- res.writeHead(502, {
1166
- 'Content-Type': 'application/json; charset=utf-8',
1167
- 'Content-Length': Buffer.byteLength(body, 'utf-8')
1168
- });
1169
- res.end(body, 'utf-8');
1170
- });
1171
-
1172
- req.pipe(upstreamReq);
1173
- });
1174
-
1175
- server.on('connection', (socket) => {
1176
- connections.add(socket);
1177
- socket.on('close', () => connections.delete(socket));
1178
- });
1179
-
1180
- return new Promise((resolve, reject) => {
1181
- server.once('error', reject);
1182
- server.listen(settings.port, settings.host, () => {
1183
- server.removeListener('error', reject);
1184
- resolve({
1185
- server,
1186
- connections,
1187
- settings,
1188
- upstream,
1189
- startedAt: toIsoTime(Date.now()),
1190
- listenUrl: buildProxyListenUrl(settings)
1191
- });
1192
- });
1193
- });
1194
- }
1195
-
1196
- async function startBuiltinProxyRuntime(payload = {}) {
1197
- if (runtime) {
1198
- return {
1199
- error: '内建代理已在运行',
1200
- runtime: {
1201
- listenUrl: runtime.listenUrl,
1202
- upstreamProvider: runtime.upstream.providerName
1203
- }
1204
- };
1205
- }
1206
-
1207
- const saveResult = saveBuiltinProxySettings(payload);
1208
- if (saveResult.error) {
1209
- return { error: saveResult.error };
1210
- }
1211
- const settings = saveResult.settings;
1212
- const upstream = resolveBuiltinProxyUpstream(settings);
1213
- if (upstream.error) {
1214
- return { error: upstream.error };
1215
- }
1216
-
1217
- try {
1218
- runtime = await createBuiltinProxyServer(settings, upstream);
1219
- return {
1220
- success: true,
1221
- running: true,
1222
- listenUrl: runtime.listenUrl,
1223
- upstreamProvider: upstream.providerName,
1224
- settings
1225
- };
1226
- } catch (e) {
1227
- return { error: `启动内建代理失败: ${e.message}` };
1228
- }
1229
- }
1230
-
1231
- async function stopBuiltinProxyRuntime() {
1232
- if (!runtime) {
1233
- return { success: true, running: false };
1234
- }
1235
- const currentRuntime = runtime;
1236
- runtime = null;
1237
-
1238
- await new Promise((resolve) => {
1239
- let settled = false;
1240
- const finish = () => {
1241
- if (settled) return;
1242
- settled = true;
1243
- resolve();
1244
- };
1245
-
1246
- currentRuntime.server.close(() => finish());
1247
- setTimeout(() => finish(), 1000);
1248
- });
1249
-
1250
- for (const socket of currentRuntime.connections) {
1251
- try { socket.destroy(); } catch (_) {}
1252
- }
1253
- currentRuntime.connections.clear();
1254
-
1255
- return {
1256
- success: true,
1257
- running: false
1258
- };
1259
- }
1260
-
1261
- function getBuiltinProxyStatus() {
1262
- const settings = readBuiltinProxySettings();
1263
- return {
1264
- running: !!runtime,
1265
- settings,
1266
- runtime: runtime
1267
- ? {
1268
- provider: BUILTIN_PROXY_PROVIDER_NAME,
1269
- startedAt: runtime.startedAt,
1270
- listenUrl: runtime.listenUrl,
1271
- upstreamProvider: runtime.upstream.providerName,
1272
- upstreamBaseUrl: runtime.upstream.baseUrl
1273
- }
1274
- : null
1275
- };
1276
- }
1277
-
1278
- return {
1279
- canListenPort,
1280
- findAvailablePort,
1281
- normalizeBuiltinProxySettings,
1282
- readBuiltinProxySettings,
1283
- resolveBuiltinProxyProviderName,
1284
- saveBuiltinProxySettings,
1285
- buildProxyListenUrl,
1286
- buildBuiltinProxyProviderBaseUrl,
1287
- removePersistedBuiltinProxyProviderFromConfig,
1288
- hasCodexConfigReadyForProxy,
1289
- resolveBuiltinProxyUpstream,
1290
- createBuiltinProxyServer,
1291
- startBuiltinProxyRuntime,
1292
- stopBuiltinProxyRuntime,
1293
- getBuiltinProxyStatus
1294
- };
1295
- }
1296
-
1297
- module.exports = {
1298
- createBuiltinProxyRuntimeController
1299
- };
1
+ const http = require('http');
2
+ const net = require('net');
3
+ const crypto = require('crypto');
4
+ const toml = require('@iarna/toml');
5
+ const { readJsonFile, writeJsonAtomic } = require('../lib/cli-file-utils');
6
+ const { isValidHttpUrl, normalizeBaseUrl, joinApiUrl } = require('../lib/cli-utils');
7
+ const { toIsoTime } = require('../lib/cli-session-utils');
8
+
9
+ function createBuiltinProxyRuntimeController(deps = {}) {
10
+ const {
11
+ fs,
12
+ https,
13
+ CONFIG_FILE,
14
+ BUILTIN_PROXY_SETTINGS_FILE,
15
+ DEFAULT_BUILTIN_PROXY_SETTINGS,
16
+ BUILTIN_PROXY_PROVIDER_NAME,
17
+ CODEXMATE_MANAGED_MARKER,
18
+ HTTP_KEEP_ALIVE_AGENT,
19
+ HTTPS_KEEP_ALIVE_AGENT,
20
+ readConfig,
21
+ writeConfig,
22
+ readConfigOrVirtualDefault,
23
+ resolveAuthTokenFromCurrentProfile,
24
+ isPlainObject,
25
+ isBuiltinManagedProvider,
26
+ findProviderSectionRanges,
27
+ findProviderDescendantSectionRanges,
28
+ normalizeLegacySegments,
29
+ buildLegacySegmentsKey,
30
+ formatHostForUrl
31
+ } = deps;
32
+
33
+ if (!fs) throw new Error('createBuiltinProxyRuntimeController 缺少 fs');
34
+ if (!https) throw new Error('createBuiltinProxyRuntimeController 缺少 https');
35
+ if (!CONFIG_FILE) throw new Error('createBuiltinProxyRuntimeController 缺少 CONFIG_FILE');
36
+ if (!BUILTIN_PROXY_SETTINGS_FILE) throw new Error('createBuiltinProxyRuntimeController 缺少 BUILTIN_PROXY_SETTINGS_FILE');
37
+ if (!DEFAULT_BUILTIN_PROXY_SETTINGS || typeof DEFAULT_BUILTIN_PROXY_SETTINGS !== 'object') {
38
+ throw new Error('createBuiltinProxyRuntimeController 缺少 DEFAULT_BUILTIN_PROXY_SETTINGS');
39
+ }
40
+ if (!BUILTIN_PROXY_PROVIDER_NAME) throw new Error('createBuiltinProxyRuntimeController 缺少 BUILTIN_PROXY_PROVIDER_NAME');
41
+ if (typeof readConfig !== 'function') throw new Error('createBuiltinProxyRuntimeController 缺少 readConfig');
42
+ if (typeof writeConfig !== 'function') throw new Error('createBuiltinProxyRuntimeController 缺少 writeConfig');
43
+ if (typeof readConfigOrVirtualDefault !== 'function') {
44
+ throw new Error('createBuiltinProxyRuntimeController 缺少 readConfigOrVirtualDefault');
45
+ }
46
+ if (typeof resolveAuthTokenFromCurrentProfile !== 'function') {
47
+ throw new Error('createBuiltinProxyRuntimeController 缺少 resolveAuthTokenFromCurrentProfile');
48
+ }
49
+ if (typeof isPlainObject !== 'function') throw new Error('createBuiltinProxyRuntimeController 缺少 isPlainObject');
50
+ if (typeof isBuiltinManagedProvider !== 'function') {
51
+ throw new Error('createBuiltinProxyRuntimeController 缺少 isBuiltinManagedProvider');
52
+ }
53
+ if (typeof findProviderSectionRanges !== 'function') {
54
+ throw new Error('createBuiltinProxyRuntimeController 缺少 findProviderSectionRanges');
55
+ }
56
+ if (typeof findProviderDescendantSectionRanges !== 'function') {
57
+ throw new Error('createBuiltinProxyRuntimeController 缺少 findProviderDescendantSectionRanges');
58
+ }
59
+ if (typeof normalizeLegacySegments !== 'function') {
60
+ throw new Error('createBuiltinProxyRuntimeController 缺少 normalizeLegacySegments');
61
+ }
62
+ if (typeof buildLegacySegmentsKey !== 'function') {
63
+ throw new Error('createBuiltinProxyRuntimeController 缺少 buildLegacySegmentsKey');
64
+ }
65
+ if (typeof formatHostForUrl !== 'function') throw new Error('createBuiltinProxyRuntimeController 缺少 formatHostForUrl');
66
+
67
+ let runtime = null;
68
+
69
+ function readRequestBody(req, maxBytes) {
70
+ return new Promise((resolve) => {
71
+ let body = '';
72
+ let size = 0;
73
+ let aborted = false;
74
+ req.on('data', (chunk) => {
75
+ if (aborted) return;
76
+ size += chunk.length;
77
+ if (Number.isFinite(maxBytes) && maxBytes > 0 && size > maxBytes) {
78
+ aborted = true;
79
+ try { req.destroy(); } catch (_) {}
80
+ resolve({ error: '请求体过大' });
81
+ return;
82
+ }
83
+ body += chunk;
84
+ });
85
+ req.on('end', () => {
86
+ if (aborted) return;
87
+ resolve({ body });
88
+ });
89
+ req.on('error', (err) => resolve({ error: err && err.message ? err.message : 'request failed' }));
90
+ });
91
+ }
92
+
93
+ function parseJsonOrError(text) {
94
+ if (typeof text !== 'string' || !text.trim()) {
95
+ return { value: null, error: 'empty body' };
96
+ }
97
+ try {
98
+ return { value: JSON.parse(text), error: '' };
99
+ } catch (e) {
100
+ return { value: null, error: e && e.message ? e.message : 'invalid json' };
101
+ }
102
+ }
103
+
104
+ function shouldFallbackFromUpstreamResponses(status, bodyText) {
105
+ if (!Number.isFinite(status)) return false;
106
+ if (status === 404 || status === 405 || status === 501) return true;
107
+ const text = String(bodyText || '');
108
+ if (!text) return false;
109
+ if (/not implemented/i.test(text)) return true;
110
+ if (/convert_request_failed/i.test(text)) return true;
111
+ try {
112
+ const parsed = JSON.parse(text);
113
+ const code = parsed && parsed.error && typeof parsed.error.code === 'string' ? parsed.error.code : '';
114
+ const msg = parsed && parsed.error && typeof parsed.error.message === 'string' ? parsed.error.message : '';
115
+ if (code === 'convert_request_failed') return true;
116
+ if (/not implemented/i.test(msg)) return true;
117
+ } catch (_) {}
118
+ return false;
119
+ }
120
+
121
+ function shouldFallbackFromUpstreamResponsesFailure(error) {
122
+ const text = String(error || '').trim();
123
+ if (!text) return false;
124
+ if (/timeout/i.test(text)) return true;
125
+ if (/socket hang up/i.test(text)) return true;
126
+ if (/ECONNRESET/i.test(text)) return true;
127
+ return false;
128
+ }
129
+
130
+ function proxyRequestJson(targetUrl, options = {}) {
131
+ const parsed = new URL(targetUrl);
132
+ const transport = parsed.protocol === 'https:' ? https : http;
133
+ const bodyText = options.body ? JSON.stringify(options.body) : '';
134
+ const headers = {
135
+ 'Accept': 'application/json',
136
+ ...(options.body ? { 'Content-Type': 'application/json' } : {}),
137
+ ...(options.headers || {})
138
+ };
139
+ if (options.body) {
140
+ headers['Content-Length'] = Buffer.byteLength(bodyText, 'utf-8');
141
+ }
142
+ const timeoutMs = Number.isFinite(options.timeoutMs)
143
+ ? Math.max(1000, Number(options.timeoutMs))
144
+ : 30000;
145
+
146
+ return new Promise((resolve) => {
147
+ let settled = false;
148
+ const finish = (value) => {
149
+ if (settled) return;
150
+ settled = true;
151
+ resolve(value);
152
+ };
153
+ const req = transport.request({
154
+ protocol: parsed.protocol,
155
+ hostname: parsed.hostname,
156
+ port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
157
+ method: options.method || 'GET',
158
+ path: `${parsed.pathname}${parsed.search}`,
159
+ headers,
160
+ agent: parsed.protocol === 'https:' ? HTTPS_KEEP_ALIVE_AGENT : HTTP_KEEP_ALIVE_AGENT
161
+ }, (upstreamRes) => {
162
+ const chunks = [];
163
+ upstreamRes.on('data', (chunk) => chunk && chunks.push(chunk));
164
+ upstreamRes.on('end', () => {
165
+ const text = chunks.length ? Buffer.concat(chunks).toString('utf-8') : '';
166
+ finish({
167
+ ok: true,
168
+ status: upstreamRes.statusCode || 0,
169
+ headers: upstreamRes.headers || {},
170
+ bodyText: text
171
+ });
172
+ });
173
+ });
174
+ req.setTimeout(timeoutMs, () => {
175
+ try { req.destroy(new Error('timeout')); } catch (_) {}
176
+ finish({ ok: false, error: 'timeout' });
177
+ });
178
+ req.on('error', (err) => finish({ ok: false, error: err && err.message ? err.message : 'request failed' }));
179
+ if (bodyText) {
180
+ req.write(bodyText);
181
+ }
182
+ req.end();
183
+ });
184
+ }
185
+
186
+ function buildUpstreamUrlCandidates(baseUrl, pathSuffix) {
187
+ const safeSuffix = String(pathSuffix || '').replace(/^\/+/, '');
188
+ const candidates = [];
189
+ const push = (url) => {
190
+ if (url && !candidates.includes(url)) {
191
+ candidates.push(url);
192
+ }
193
+ };
194
+ push(joinApiUrl(baseUrl, safeSuffix));
195
+ const trimmed = normalizeBaseUrl(baseUrl);
196
+ if (trimmed && safeSuffix) {
197
+ push(`${trimmed}/${safeSuffix}`);
198
+ }
199
+ return candidates;
200
+ }
201
+
202
+ async function proxyRequestJsonWithFallbackUrls(baseUrl, pathSuffix, options = {}) {
203
+ const urls = buildUpstreamUrlCandidates(baseUrl, pathSuffix);
204
+ if (urls.length === 0) {
205
+ return { ok: false, error: 'failed to build upstream URL' };
206
+ }
207
+ let lastResult = null;
208
+ for (let index = 0; index < urls.length; index += 1) {
209
+ const result = await proxyRequestJson(urls[index], options);
210
+ lastResult = result;
211
+ if (!result.ok) {
212
+ return result;
213
+ }
214
+ if (!(result.status === 404 || result.status === 405)) {
215
+ return result;
216
+ }
217
+ }
218
+ return lastResult || { ok: false, error: 'failed to build upstream URL' };
219
+ }
220
+
221
+ function stringifyJsonValue(value, fallback = '') {
222
+ if (typeof value === 'string') return value;
223
+ if (value == null) return fallback;
224
+ try {
225
+ return JSON.stringify(value);
226
+ } catch (_) {
227
+ return fallback;
228
+ }
229
+ }
230
+
231
+ function normalizeChatUsageToResponsesUsage(usage) {
232
+ if (!usage || typeof usage !== 'object' || Array.isArray(usage)) return undefined;
233
+ const pickNumber = (...keys) => {
234
+ for (const key of keys) {
235
+ if (Number.isFinite(usage[key])) return usage[key];
236
+ }
237
+ return undefined;
238
+ };
239
+ const inputTokens = pickNumber('input_tokens', 'prompt_tokens');
240
+ const outputTokens = pickNumber('output_tokens', 'completion_tokens');
241
+ const totalTokens = pickNumber('total_tokens');
242
+ const result = {};
243
+ if (inputTokens != null) result.input_tokens = inputTokens;
244
+ if (outputTokens != null) result.output_tokens = outputTokens;
245
+ if (totalTokens != null) result.total_tokens = totalTokens;
246
+ if (usage.input_tokens_details && typeof usage.input_tokens_details === 'object') {
247
+ result.input_tokens_details = usage.input_tokens_details;
248
+ } else if (usage.prompt_tokens_details && typeof usage.prompt_tokens_details === 'object') {
249
+ result.input_tokens_details = usage.prompt_tokens_details;
250
+ }
251
+ if (usage.output_tokens_details && typeof usage.output_tokens_details === 'object') {
252
+ result.output_tokens_details = usage.output_tokens_details;
253
+ } else if (usage.completion_tokens_details && typeof usage.completion_tokens_details === 'object') {
254
+ result.output_tokens_details = usage.completion_tokens_details;
255
+ }
256
+ return Object.keys(result).length > 0 ? result : usage;
257
+ }
258
+
259
+ function mapChatFinishReasonToResponses(choice) {
260
+ const finishReason = choice && typeof choice === 'object' && typeof choice.finish_reason === 'string'
261
+ ? choice.finish_reason
262
+ : '';
263
+ if (finishReason === 'length') {
264
+ return { status: 'incomplete', incomplete_details: { reason: 'max_output_tokens' } };
265
+ }
266
+ if (finishReason === 'content_filter') {
267
+ return { status: 'incomplete', incomplete_details: { reason: 'content_filter' } };
268
+ }
269
+ return { status: 'completed' };
270
+ }
271
+
272
+ function normalizeChatMessageContentToResponsesContent(content, refusal = '') {
273
+ const blocks = [];
274
+ const pushText = (text) => {
275
+ if (typeof text === 'string' && text) {
276
+ blocks.push({ type: 'output_text', text });
277
+ }
278
+ };
279
+ if (typeof content === 'string') {
280
+ pushText(content);
281
+ } else if (Array.isArray(content)) {
282
+ for (const item of content) {
283
+ if (!item) continue;
284
+ if (typeof item === 'string') {
285
+ pushText(item);
286
+ continue;
287
+ }
288
+ if (typeof item !== 'object') continue;
289
+ const type = typeof item.type === 'string' ? item.type : '';
290
+ if ((type === 'text' || type === 'output_text') && typeof item.text === 'string') {
291
+ pushText(item.text);
292
+ continue;
293
+ }
294
+ if (typeof item.content === 'string') {
295
+ pushText(item.content);
296
+ }
297
+ }
298
+ }
299
+ if (typeof refusal === 'string' && refusal) {
300
+ blocks.push({ type: 'refusal', refusal });
301
+ }
302
+ return blocks;
303
+ }
304
+
305
+ function buildResponsesPayloadFromChatCompletion(payload, fallbackModel = '') {
306
+ const base = payload && typeof payload === 'object' && !Array.isArray(payload) ? payload : {};
307
+ const choice = Array.isArray(base.choices) ? base.choices[0] : null;
308
+ const message = choice && typeof choice === 'object' && choice.message && typeof choice.message === 'object'
309
+ ? choice.message
310
+ : {};
311
+ const output = [];
312
+ const messageContent = normalizeChatMessageContentToResponsesContent(message.content, message.refusal);
313
+ if (messageContent.length > 0 || !Array.isArray(message.tool_calls) || message.tool_calls.length === 0) {
314
+ output.push({
315
+ type: 'message',
316
+ role: 'assistant',
317
+ content: messageContent.length > 0 ? messageContent : [{ type: 'output_text', text: '' }]
318
+ });
319
+ }
320
+ if (Array.isArray(message.tool_calls)) {
321
+ for (const toolCall of message.tool_calls) {
322
+ if (!toolCall || typeof toolCall !== 'object') continue;
323
+ const fn = toolCall.function && typeof toolCall.function === 'object' ? toolCall.function : {};
324
+ const name = typeof fn.name === 'string' ? fn.name : '';
325
+ if (!name) continue;
326
+ output.push({
327
+ type: 'function_call',
328
+ call_id: typeof toolCall.id === 'string' && toolCall.id ? toolCall.id : `call_${crypto.randomBytes(8).toString('hex')}`,
329
+ name,
330
+ arguments: stringifyJsonValue(fn.arguments, '{}')
331
+ });
332
+ }
333
+ }
334
+ const finish = mapChatFinishReasonToResponses(choice);
335
+ return ensureResponseMetadata({
336
+ id: typeof base.id === 'string' ? base.id : undefined,
337
+ model: typeof base.model === 'string' ? base.model : fallbackModel,
338
+ status: finish.status,
339
+ ...(finish.incomplete_details ? { incomplete_details: finish.incomplete_details } : {}),
340
+ output,
341
+ usage: normalizeChatUsageToResponsesUsage(base.usage)
342
+ });
343
+ }
344
+
345
+ function normalizeResponsesInputToChatMessages(input) {
346
+ // 参考 cc-switch Responses 转换形态:message content 保持为消息,function_call /
347
+ // function_call_output 提升为 OpenAI Chat 的 assistant tool_calls / tool 消息。
348
+ const toChatContent = (blocks) => {
349
+ if (!Array.isArray(blocks)) return '';
350
+ const out = [];
351
+ for (const block of blocks) {
352
+ if (!block || typeof block !== 'object') continue;
353
+ const type = typeof block.type === 'string' ? block.type : '';
354
+ if ((type === 'input_text' || type === 'output_text' || type === 'text') && typeof block.text === 'string') {
355
+ out.push({ type: 'text', text: block.text });
356
+ continue;
357
+ }
358
+ if (type === 'refusal' && typeof block.refusal === 'string') {
359
+ out.push({ type: 'text', text: block.refusal });
360
+ continue;
361
+ }
362
+ if (type === 'input_image') {
363
+ const raw = block.image_url != null ? block.image_url : block.imageUrl;
364
+ const url = typeof raw === 'string'
365
+ ? raw
366
+ : (raw && typeof raw === 'object' && typeof raw.url === 'string' ? raw.url : '');
367
+ if (url) {
368
+ out.push({ type: 'image_url', image_url: { url } });
369
+ }
370
+ continue;
371
+ }
372
+ if (type === 'image_url' && block.image_url) {
373
+ out.push({ type: 'image_url', image_url: block.image_url });
374
+ }
375
+ }
376
+ if (out.length === 0) return '';
377
+ return out;
378
+ };
379
+
380
+ const messageFromResponsesItem = (item) => {
381
+ if (!item || typeof item !== 'object') return null;
382
+ const type = typeof item.type === 'string' ? item.type : '';
383
+ if (type === 'function_call') {
384
+ const name = typeof item.name === 'string' ? item.name : '';
385
+ if (!name) return null;
386
+ return {
387
+ role: 'assistant',
388
+ content: null,
389
+ tool_calls: [{
390
+ id: typeof item.call_id === 'string' && item.call_id ? item.call_id : (typeof item.id === 'string' ? item.id : `call_${crypto.randomBytes(8).toString('hex')}`),
391
+ type: 'function',
392
+ function: {
393
+ name,
394
+ arguments: stringifyJsonValue(item.arguments, '{}')
395
+ }
396
+ }]
397
+ };
398
+ }
399
+ if (type === 'function_call_output') {
400
+ const callId = typeof item.call_id === 'string' ? item.call_id : '';
401
+ return {
402
+ role: 'tool',
403
+ tool_call_id: callId,
404
+ content: stringifyJsonValue(item.output, '')
405
+ };
406
+ }
407
+ if (typeof item.role === 'string' && item.content != null) {
408
+ const role = item.role.trim() || 'user';
409
+ const content = Array.isArray(item.content)
410
+ ? toChatContent(item.content)
411
+ : item.content;
412
+ return content || content === null ? { role, content } : null;
413
+ }
414
+ if (type) {
415
+ const content = toChatContent([item]);
416
+ return content ? { role: 'user', content } : null;
417
+ }
418
+ return null;
419
+ };
420
+
421
+ if (typeof input === 'string') {
422
+ return [{ role: 'user', content: input }];
423
+ }
424
+ if (input && typeof input === 'object' && !Array.isArray(input)) {
425
+ const message = messageFromResponsesItem(input);
426
+ return message ? [message] : [];
427
+ }
428
+ if (!Array.isArray(input)) {
429
+ return [];
430
+ }
431
+
432
+ const messages = [];
433
+ for (const item of input) {
434
+ const message = messageFromResponsesItem(item);
435
+ if (message) messages.push(message);
436
+ }
437
+ if (messages.length > 0) {
438
+ return messages;
439
+ }
440
+
441
+ const fallbackContent = toChatContent(input);
442
+ if (fallbackContent) {
443
+ return [{ role: 'user', content: fallbackContent }];
444
+ }
445
+ return [];
446
+ }
447
+
448
+ function normalizeResponsesToolsToChatTools(tools) {
449
+ if (!Array.isArray(tools)) return tools;
450
+ return tools
451
+ .map((tool) => {
452
+ if (!tool || typeof tool !== 'object') return null;
453
+ if (tool.type !== 'function') return tool;
454
+ const sourceFn = tool.function && typeof tool.function === 'object' && !Array.isArray(tool.function)
455
+ ? tool.function
456
+ : {};
457
+ const name = typeof sourceFn.name === 'string' && sourceFn.name.trim()
458
+ ? sourceFn.name.trim()
459
+ : (typeof tool.name === 'string' ? tool.name.trim() : '');
460
+ if (!name) return null;
461
+ const description = typeof sourceFn.description === 'string'
462
+ ? sourceFn.description
463
+ : (typeof tool.description === 'string' ? tool.description : undefined);
464
+ const parameters = sourceFn.parameters && typeof sourceFn.parameters === 'object' && !Array.isArray(sourceFn.parameters)
465
+ ? sourceFn.parameters
466
+ : (tool.parameters && typeof tool.parameters === 'object' && !Array.isArray(tool.parameters) ? tool.parameters : {});
467
+ const strict = typeof sourceFn.strict === 'boolean'
468
+ ? sourceFn.strict
469
+ : (typeof tool.strict === 'boolean' ? tool.strict : undefined);
470
+ const fn = { name, parameters };
471
+ if (description !== undefined) fn.description = description;
472
+ if (strict !== undefined) fn.strict = strict;
473
+ return { type: 'function', function: fn };
474
+ })
475
+ .filter(Boolean);
476
+ }
477
+
478
+ function normalizeResponsesToolChoiceToChatToolChoice(toolChoice) {
479
+ if (!toolChoice || typeof toolChoice !== 'object' || Array.isArray(toolChoice)) return toolChoice;
480
+ if (toolChoice.type === 'function' && typeof toolChoice.name === 'string') {
481
+ return { type: 'function', function: { name: toolChoice.name } };
482
+ }
483
+ return toolChoice;
484
+ }
485
+
486
+ function buildChatCompletionsBodyFromResponsesPayload(payload) {
487
+ const source = payload && typeof payload === 'object' && !Array.isArray(payload) ? payload : {};
488
+ const messages = normalizeResponsesInputToChatMessages(source.input);
489
+ const instructions = typeof source.instructions === 'string' ? source.instructions.trim() : '';
490
+ if (instructions) {
491
+ messages.unshift({ role: 'system', content: instructions });
492
+ }
493
+
494
+ const chatBody = {
495
+ model: typeof source.model === 'string' ? source.model : '',
496
+ messages,
497
+ stream: false
498
+ };
499
+
500
+ const passthroughKeys = [
501
+ 'frequency_penalty',
502
+ 'presence_penalty',
503
+ 'response_format',
504
+ 'stop',
505
+ 'temperature',
506
+ 'top_p',
507
+ 'tools',
508
+ 'tool_choice',
509
+ 'logprobs',
510
+ 'top_logprobs',
511
+ 'kbs',
512
+ 'is_online',
513
+ 'user',
514
+ 'seed',
515
+ 'n',
516
+ 'modalities',
517
+ 'audio',
518
+ 'reasoning_effort'
519
+ ];
520
+ for (const key of passthroughKeys) {
521
+ if (Object.prototype.hasOwnProperty.call(source, key)) {
522
+ if (key === 'tools') {
523
+ chatBody[key] = normalizeResponsesToolsToChatTools(source[key]);
524
+ } else if (key === 'tool_choice') {
525
+ chatBody[key] = normalizeResponsesToolChoiceToChatToolChoice(source[key]);
526
+ } else {
527
+ chatBody[key] = source[key];
528
+ }
529
+ }
530
+ }
531
+
532
+ if (Object.prototype.hasOwnProperty.call(source, 'max_tokens')) {
533
+ chatBody.max_tokens = source.max_tokens;
534
+ } else if (source.max_output_tokens != null) {
535
+ chatBody.max_tokens = source.max_output_tokens;
536
+ }
537
+
538
+ return chatBody;
539
+ }
540
+
541
+ function ensureResponseMetadata(payload) {
542
+ const base = payload && typeof payload === 'object' && !Array.isArray(payload) ? payload : {};
543
+ const id = typeof base.id === 'string' && base.id.trim()
544
+ ? base.id.trim()
545
+ : `resp_${crypto.randomBytes(10).toString('hex')}`;
546
+ const model = typeof base.model === 'string' ? base.model : '';
547
+ return {
548
+ object: 'response',
549
+ id,
550
+ model,
551
+ ...base
552
+ };
553
+ }
554
+
555
+ function writeSse(res, eventName, dataObj) {
556
+ if (eventName) {
557
+ res.write(`event: ${eventName}\n`);
558
+ }
559
+ if (dataObj === '[DONE]') {
560
+ res.write('data: [DONE]\n\n');
561
+ return;
562
+ }
563
+ res.write(`data: ${JSON.stringify(dataObj)}\n\n`);
564
+ }
565
+
566
+ function sendResponsesSse(res, responsePayload) {
567
+ const response = ensureResponseMetadata(responsePayload);
568
+ const responseId = response.id;
569
+ const model = response.model;
570
+ let sequence = 0;
571
+ const nextSeq = () => {
572
+ sequence += 1;
573
+ return sequence;
574
+ };
575
+
576
+ writeSse(res, 'response.created', {
577
+ type: 'response.created',
578
+ response: {
579
+ id: responseId,
580
+ model,
581
+ created_at: response.created_at
582
+ }
583
+ });
584
+
585
+ const output = Array.isArray(response.output) ? response.output : [];
586
+ for (let outputIndex = 0; outputIndex < output.length; outputIndex += 1) {
587
+ const item = output[outputIndex];
588
+ if (!item || typeof item !== 'object') continue;
589
+ const itemType = typeof item.type === 'string' ? item.type : '';
590
+ const itemId = typeof item.id === 'string' && item.id.trim()
591
+ ? item.id.trim()
592
+ : `item_${crypto.randomBytes(8).toString('hex')}`;
593
+
594
+ writeSse(res, 'response.output_item.added', {
595
+ type: 'response.output_item.added',
596
+ output_index: outputIndex,
597
+ item: { ...item, id: itemId }
598
+ });
599
+
600
+ if (itemType === 'message') {
601
+ const content = Array.isArray(item.content) ? item.content : [];
602
+ for (let contentIndex = 0; contentIndex < content.length; contentIndex += 1) {
603
+ const block = content[contentIndex];
604
+ if (!block || typeof block !== 'object') continue;
605
+ if (block.type !== 'output_text') continue;
606
+ const text = typeof block.text === 'string' ? block.text : '';
607
+ if (text) {
608
+ writeSse(res, 'response.output_text.delta', {
609
+ type: 'response.output_text.delta',
610
+ item_id: itemId,
611
+ output_index: outputIndex,
612
+ content_index: contentIndex,
613
+ delta: text,
614
+ sequence_number: nextSeq()
615
+ });
616
+ }
617
+ writeSse(res, 'response.output_text.done', {
618
+ type: 'response.output_text.done',
619
+ item_id: itemId,
620
+ output_index: outputIndex,
621
+ content_index: contentIndex,
622
+ text,
623
+ sequence_number: nextSeq()
624
+ });
625
+ }
626
+ }
627
+
628
+ writeSse(res, 'response.output_item.done', {
629
+ type: 'response.output_item.done',
630
+ output_index: outputIndex,
631
+ item: { ...item, id: itemId },
632
+ sequence_number: nextSeq()
633
+ });
634
+ }
635
+
636
+ writeSse(res, 'response.completed', { type: 'response.completed', response });
637
+ writeSse(res, 'done', '[DONE]');
638
+ }
639
+
640
+ function appendChatStreamToolCall(target, toolCall) {
641
+ if (!toolCall || typeof toolCall !== 'object') return;
642
+ const index = Number.isFinite(toolCall.index) ? toolCall.index : target.length;
643
+ if (!target[index]) {
644
+ target[index] = {
645
+ id: '',
646
+ type: 'function',
647
+ function: { name: '', arguments: '' }
648
+ };
649
+ }
650
+ const current = target[index];
651
+ if (typeof toolCall.id === 'string' && toolCall.id) current.id = toolCall.id;
652
+ if (typeof toolCall.type === 'string' && toolCall.type) current.type = toolCall.type;
653
+ const fn = toolCall.function && typeof toolCall.function === 'object' ? toolCall.function : null;
654
+ if (fn) {
655
+ if (typeof fn.name === 'string' && fn.name) current.function.name = fn.name;
656
+ if (typeof fn.arguments === 'string') current.function.arguments += fn.arguments;
657
+ }
658
+ }
659
+
660
+ function writeChatCompletionChunkAsResponsesSse(state, chunk) {
661
+ if (!chunk || typeof chunk !== 'object') return;
662
+ if (typeof chunk.model === 'string' && chunk.model) {
663
+ state.model = chunk.model;
664
+ }
665
+ const choices = Array.isArray(chunk.choices) ? chunk.choices : [];
666
+ for (const choice of choices) {
667
+ const delta = choice && choice.delta && typeof choice.delta === 'object' ? choice.delta : null;
668
+ if (!delta) continue;
669
+
670
+ if (typeof delta.content === 'string' && delta.content) {
671
+ if (!state.messageItem) {
672
+ state.messageItem = {
673
+ id: `msg_${crypto.randomBytes(8).toString('hex')}`,
674
+ type: 'message',
675
+ role: 'assistant',
676
+ content: [{ type: 'output_text', text: '' }]
677
+ };
678
+ state.output.push(state.messageItem);
679
+ writeSse(state.res, 'response.output_item.added', {
680
+ type: 'response.output_item.added',
681
+ output_index: state.output.length - 1,
682
+ item: state.messageItem
683
+ });
684
+ }
685
+ state.messageText += delta.content;
686
+ state.messageItem.content[0].text = state.messageText;
687
+ writeSse(state.res, 'response.output_text.delta', {
688
+ type: 'response.output_text.delta',
689
+ item_id: state.messageItem.id,
690
+ output_index: state.output.length - 1,
691
+ content_index: 0,
692
+ delta: delta.content,
693
+ sequence_number: state.nextSeq()
694
+ });
695
+ }
696
+
697
+ if (Array.isArray(delta.tool_calls)) {
698
+ for (const toolCall of delta.tool_calls) {
699
+ appendChatStreamToolCall(state.toolCalls, toolCall);
700
+ }
701
+ }
702
+ }
703
+ }
704
+
705
+ function finishChatStreamResponsesSse(state) {
706
+ if (state.finished) return;
707
+ state.finished = true;
708
+
709
+ if (state.messageItem) {
710
+ const outputIndex = state.output.indexOf(state.messageItem);
711
+ writeSse(state.res, 'response.output_text.done', {
712
+ type: 'response.output_text.done',
713
+ item_id: state.messageItem.id,
714
+ output_index: outputIndex,
715
+ content_index: 0,
716
+ text: state.messageText,
717
+ sequence_number: state.nextSeq()
718
+ });
719
+ writeSse(state.res, 'response.output_item.done', {
720
+ type: 'response.output_item.done',
721
+ output_index: outputIndex,
722
+ item: state.messageItem,
723
+ sequence_number: state.nextSeq()
724
+ });
725
+ }
726
+
727
+ for (const toolCall of state.toolCalls) {
728
+ if (!toolCall) continue;
729
+ const item = {
730
+ type: 'function_call',
731
+ call_id: toolCall.id || `call_${crypto.randomBytes(8).toString('hex')}`,
732
+ name: toolCall.function && typeof toolCall.function.name === 'string' ? toolCall.function.name : '',
733
+ arguments: toolCall.function && typeof toolCall.function.arguments === 'string' ? toolCall.function.arguments : ''
734
+ };
735
+ const outputIndex = state.output.length;
736
+ state.output.push(item);
737
+ writeSse(state.res, 'response.output_item.added', {
738
+ type: 'response.output_item.added',
739
+ output_index: outputIndex,
740
+ item
741
+ });
742
+ writeSse(state.res, 'response.output_item.done', {
743
+ type: 'response.output_item.done',
744
+ output_index: outputIndex,
745
+ item,
746
+ sequence_number: state.nextSeq()
747
+ });
748
+ }
749
+
750
+ const response = ensureResponseMetadata({
751
+ id: state.responseId,
752
+ model: state.model,
753
+ created_at: state.createdAt,
754
+ status: 'completed',
755
+ output: state.output
756
+ });
757
+ writeSse(state.res, 'response.completed', { type: 'response.completed', response });
758
+ writeSse(state.res, 'done', '[DONE]');
759
+ state.res.end();
760
+ }
761
+
762
+ function streamChatCompletionsAsResponsesSse(targetUrl, options = {}) {
763
+ const parsed = new URL(targetUrl);
764
+ const transport = parsed.protocol === 'https:' ? https : http;
765
+ const bodyText = options.body ? JSON.stringify(options.body) : '';
766
+ const headers = {
767
+ 'Accept': 'text/event-stream',
768
+ ...(options.body ? { 'Content-Type': 'application/json' } : {}),
769
+ ...(options.headers || {})
770
+ };
771
+ if (options.body) {
772
+ headers['Content-Length'] = Buffer.byteLength(bodyText, 'utf-8');
773
+ }
774
+ const timeoutMs = Number.isFinite(options.timeoutMs)
775
+ ? Math.max(1000, Number(options.timeoutMs))
776
+ : 30000;
777
+ const res = options.res;
778
+ const model = typeof options.model === 'string' ? options.model : '';
779
+
780
+ return new Promise((resolve) => {
781
+ let settled = false;
782
+ const finish = (value) => {
783
+ if (settled) return;
784
+ settled = true;
785
+ resolve(value);
786
+ };
787
+ const req = transport.request({
788
+ protocol: parsed.protocol,
789
+ hostname: parsed.hostname,
790
+ port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
791
+ method: options.method || 'POST',
792
+ path: `${parsed.pathname}${parsed.search}`,
793
+ headers,
794
+ agent: parsed.protocol === 'https:' ? HTTPS_KEEP_ALIVE_AGENT : HTTP_KEEP_ALIVE_AGENT
795
+ }, (upstreamRes) => {
796
+ const status = upstreamRes.statusCode || 0;
797
+ const chunks = [];
798
+ const contentType = String(upstreamRes.headers && upstreamRes.headers['content-type'] || '');
799
+
800
+ if (status === 404 || status === 405) {
801
+ upstreamRes.on('data', (chunk) => chunk && chunks.push(chunk));
802
+ upstreamRes.on('end', () => finish({ retry: true, status, bodyText: chunks.length ? Buffer.concat(chunks).toString('utf-8') : '' }));
803
+ return;
804
+ }
805
+
806
+ if (status >= 400) {
807
+ upstreamRes.on('data', (chunk) => chunk && chunks.push(chunk));
808
+ upstreamRes.on('end', () => finish({ ok: false, status, bodyText: chunks.length ? Buffer.concat(chunks).toString('utf-8') : '' }));
809
+ return;
810
+ }
811
+
812
+ res.writeHead(200, {
813
+ 'Content-Type': 'text/event-stream; charset=utf-8',
814
+ 'Cache-Control': 'no-cache',
815
+ 'Connection': 'keep-alive',
816
+ 'X-Accel-Buffering': 'no'
817
+ });
818
+
819
+ if (!/text\/event-stream/i.test(contentType)) {
820
+ upstreamRes.on('data', (chunk) => chunk && chunks.push(chunk));
821
+ upstreamRes.on('end', () => {
822
+ const text = chunks.length ? Buffer.concat(chunks).toString('utf-8') : '';
823
+ const parsedJson = parseJsonOrError(text);
824
+ if (parsedJson.error) {
825
+ writeSse(res, 'response.failed', { type: 'response.failed', error: `invalid upstream response: ${parsedJson.error}` });
826
+ writeSse(res, 'done', '[DONE]');
827
+ res.end();
828
+ finish({ ok: true });
829
+ return;
830
+ }
831
+ sendResponsesSse(res, buildResponsesPayloadFromChatCompletion(parsedJson.value, model));
832
+ res.end();
833
+ finish({ ok: true });
834
+ });
835
+ return;
836
+ }
837
+
838
+ let sequence = 0;
839
+ const state = {
840
+ res,
841
+ responseId: `resp_${crypto.randomBytes(10).toString('hex')}`,
842
+ model,
843
+ createdAt: Math.floor(Date.now() / 1000),
844
+ output: [],
845
+ messageItem: null,
846
+ messageText: '',
847
+ toolCalls: [],
848
+ finished: false,
849
+ nextSeq: () => {
850
+ sequence += 1;
851
+ return sequence;
852
+ }
853
+ };
854
+ writeSse(res, 'response.created', {
855
+ type: 'response.created',
856
+ response: {
857
+ id: state.responseId,
858
+ model: state.model,
859
+ created_at: state.createdAt
860
+ }
861
+ });
862
+
863
+ let buffer = '';
864
+ const handleEventBlock = (block) => {
865
+ const dataLines = String(block || '')
866
+ .split(/\r?\n/)
867
+ .filter((line) => line.startsWith('data:'))
868
+ .map((line) => line.slice(5).trimStart());
869
+ if (dataLines.length === 0) return;
870
+ const data = dataLines.join('\n').trim();
871
+ if (!data) return;
872
+ if (data === '[DONE]') {
873
+ finishChatStreamResponsesSse(state);
874
+ finish({ ok: true });
875
+ return;
876
+ }
877
+ const parsedChunk = parseJsonOrError(data);
878
+ if (!parsedChunk.error) {
879
+ writeChatCompletionChunkAsResponsesSse(state, parsedChunk.value);
880
+ }
881
+ };
882
+
883
+ upstreamRes.on('data', (chunk) => {
884
+ buffer += chunk.toString('utf-8');
885
+ let boundary = buffer.search(/\r?\n\r?\n/);
886
+ while (boundary >= 0) {
887
+ const block = buffer.slice(0, boundary);
888
+ const match = buffer.slice(boundary).match(/^\r?\n\r?\n/);
889
+ buffer = buffer.slice(boundary + (match ? match[0].length : 2));
890
+ handleEventBlock(block);
891
+ boundary = buffer.search(/\r?\n\r?\n/);
892
+ }
893
+ });
894
+ upstreamRes.on('end', () => {
895
+ if (buffer.trim()) handleEventBlock(buffer);
896
+ finishChatStreamResponsesSse(state);
897
+ finish({ ok: true });
898
+ });
899
+ });
900
+ req.setTimeout(timeoutMs, () => {
901
+ try { req.destroy(new Error('timeout')); } catch (_) {}
902
+ finish({ ok: false, error: 'timeout' });
903
+ });
904
+ req.on('error', (err) => finish({ ok: false, error: err && err.message ? err.message : 'request failed' }));
905
+ if (bodyText) req.write(bodyText);
906
+ req.end();
907
+ });
908
+ }
909
+
910
+ async function streamChatCompletionsAsResponsesSseWithFallbackUrls(baseUrl, pathSuffix, options = {}) {
911
+ const urls = buildUpstreamUrlCandidates(baseUrl, pathSuffix);
912
+ if (urls.length === 0) {
913
+ return { ok: false, error: 'failed to build upstream URL' };
914
+ }
915
+ let lastResult = null;
916
+ for (const url of urls) {
917
+ const result = await streamChatCompletionsAsResponsesSse(url, options);
918
+ lastResult = result;
919
+ if (result && result.retry) continue;
920
+ return result;
921
+ }
922
+ return lastResult || { ok: false, error: 'failed to build upstream URL' };
923
+ }
924
+
925
+ function canListenPort(host, port) {
926
+ return new Promise((resolve) => {
927
+ const tester = net.createServer();
928
+ tester.unref();
929
+ tester.once('error', () => resolve(false));
930
+ tester.once('listening', () => {
931
+ tester.close(() => resolve(true));
932
+ });
933
+ tester.listen(port, host);
934
+ });
935
+ }
936
+
937
+ async function findAvailablePort(host, startPort, maxAttempts = 20) {
938
+ const start = parseInt(String(startPort), 10);
939
+ if (!Number.isFinite(start) || start <= 0) {
940
+ return 0;
941
+ }
942
+ const attempts = Number.isFinite(maxAttempts) && maxAttempts > 0 ? maxAttempts : 20;
943
+ for (let offset = 0; offset < attempts; offset += 1) {
944
+ const candidate = start + offset;
945
+ if (candidate > 65535) {
946
+ break;
947
+ }
948
+ // eslint-disable-next-line no-await-in-loop
949
+ const ok = await canListenPort(host, candidate);
950
+ if (ok) {
951
+ return candidate;
952
+ }
953
+ }
954
+ return 0;
955
+ }
956
+
957
+ function resolveBuiltinProxyProviderName(rawProviderName, providers = {}, preferredProvider = '') {
958
+ const providerMap = providers && isPlainObject(providers) ? providers : {};
959
+ const providerNames = Object.keys(providerMap)
960
+ .filter((name) => name && !isBuiltinManagedProvider(name));
961
+ const requested = typeof rawProviderName === 'string' ? rawProviderName.trim() : '';
962
+ if (requested && !isBuiltinManagedProvider(requested) && providerMap[requested]) {
963
+ return requested;
964
+ }
965
+ const preferred = typeof preferredProvider === 'string' ? preferredProvider.trim() : '';
966
+ if (preferred && !isBuiltinManagedProvider(preferred) && providerMap[preferred]) {
967
+ return preferred;
968
+ }
969
+ return providerNames[0] || '';
970
+ }
971
+
972
+ function normalizeBuiltinProxySettings(raw) {
973
+ const merged = {
974
+ ...DEFAULT_BUILTIN_PROXY_SETTINGS,
975
+ ...(isPlainObject(raw) ? raw : {})
976
+ };
977
+ const host = typeof merged.host === 'string' ? merged.host.trim() : '';
978
+ const port = parseInt(String(merged.port), 10);
979
+ const provider = typeof merged.provider === 'string' ? merged.provider.trim() : '';
980
+ const authSourceRaw = typeof merged.authSource === 'string' ? merged.authSource.trim().toLowerCase() : '';
981
+ const timeoutMs = parseInt(String(merged.timeoutMs), 10);
982
+ const authSource = authSourceRaw === 'profile' || authSourceRaw === 'none' ? authSourceRaw : 'provider';
983
+
984
+ return {
985
+ enabled: merged.enabled !== false,
986
+ host: host || DEFAULT_BUILTIN_PROXY_SETTINGS.host,
987
+ port: Number.isFinite(port) && port > 0 && port <= 65535 ? port : DEFAULT_BUILTIN_PROXY_SETTINGS.port,
988
+ provider,
989
+ authSource,
990
+ timeoutMs: Number.isFinite(timeoutMs) && timeoutMs >= 1000
991
+ ? timeoutMs
992
+ : DEFAULT_BUILTIN_PROXY_SETTINGS.timeoutMs
993
+ };
994
+ }
995
+
996
+ function readBuiltinProxySettings() {
997
+ const parsed = readJsonFile(BUILTIN_PROXY_SETTINGS_FILE, null);
998
+ return normalizeBuiltinProxySettings(parsed);
999
+ }
1000
+
1001
+ function saveBuiltinProxySettings(payload = {}, options = {}) {
1002
+ const current = readBuiltinProxySettings();
1003
+ const merged = normalizeBuiltinProxySettings({
1004
+ ...current,
1005
+ ...(isPlainObject(payload) ? payload : {})
1006
+ });
1007
+
1008
+ if (!merged.host) {
1009
+ return { error: '代理 host 不能为空' };
1010
+ }
1011
+ if (!Number.isFinite(merged.port) || merged.port <= 0 || merged.port > 65535) {
1012
+ return { error: '代理端口无效(1-65535)' };
1013
+ }
1014
+
1015
+ const { config } = readConfigOrVirtualDefault();
1016
+ const providers = config && isPlainObject(config.model_providers) ? config.model_providers : {};
1017
+ const preferredProvider = typeof config.model_provider === 'string' ? config.model_provider.trim() : '';
1018
+ const finalProvider = resolveBuiltinProxyProviderName(merged.provider, providers, preferredProvider);
1019
+
1020
+ const normalized = {
1021
+ ...merged,
1022
+ provider: finalProvider
1023
+ };
1024
+
1025
+ if (!options.skipWrite) {
1026
+ writeJsonAtomic(BUILTIN_PROXY_SETTINGS_FILE, normalized);
1027
+ }
1028
+
1029
+ return {
1030
+ success: true,
1031
+ settings: normalized
1032
+ };
1033
+ }
1034
+
1035
+ function buildProxyListenUrl(settings) {
1036
+ const host = formatHostForUrl(settings.host || DEFAULT_BUILTIN_PROXY_SETTINGS.host);
1037
+ return `http://${host}:${settings.port}`;
1038
+ }
1039
+
1040
+ function buildBuiltinProxyProviderBaseUrl(settings) {
1041
+ return `${buildProxyListenUrl(settings).replace(/\/+$/, '')}/v1`;
1042
+ }
1043
+
1044
+ function removePersistedBuiltinProxyProviderFromConfig() {
1045
+ if (!fs.existsSync(CONFIG_FILE)) {
1046
+ return { success: true, removed: false };
1047
+ }
1048
+
1049
+ let config;
1050
+ try {
1051
+ config = readConfig();
1052
+ } catch (e) {
1053
+ return { error: e.message || '读取 config.toml 失败' };
1054
+ }
1055
+
1056
+ if (!config.model_providers || !config.model_providers[BUILTIN_PROXY_PROVIDER_NAME]) {
1057
+ return { success: true, removed: false };
1058
+ }
1059
+
1060
+ const content = fs.readFileSync(CONFIG_FILE, 'utf-8');
1061
+ const lineEnding = content.includes('\r\n') ? '\r\n' : '\n';
1062
+ const hasBom = content.charCodeAt(0) === 0xFEFF;
1063
+ const providerConfig = config.model_providers[BUILTIN_PROXY_PROVIDER_NAME];
1064
+ const providerSegments = providerConfig && Array.isArray(providerConfig.__codexmate_legacy_segments)
1065
+ ? providerConfig.__codexmate_legacy_segments
1066
+ : null;
1067
+ const providerSegmentVariants = (() => {
1068
+ const variants = [];
1069
+ const seen = new Set();
1070
+ const pushVariant = (segments) => {
1071
+ const normalized = normalizeLegacySegments(segments);
1072
+ const key = buildLegacySegmentsKey(normalized);
1073
+ if (!key || seen.has(key)) return;
1074
+ seen.add(key);
1075
+ variants.push(normalized);
1076
+ };
1077
+ if (providerConfig && Array.isArray(providerConfig.__codexmate_legacy_segments)) {
1078
+ pushVariant(providerConfig.__codexmate_legacy_segments);
1079
+ }
1080
+ if (providerConfig && Array.isArray(providerConfig.__codexmate_legacy_segment_variants)) {
1081
+ for (const segments of providerConfig.__codexmate_legacy_segment_variants) {
1082
+ pushVariant(segments);
1083
+ }
1084
+ }
1085
+ if (providerSegments) {
1086
+ pushVariant(providerSegments);
1087
+ }
1088
+ if (variants.length === 0) {
1089
+ pushVariant(String(BUILTIN_PROXY_PROVIDER_NAME || '').split('.').filter((item) => item));
1090
+ }
1091
+ return variants;
1092
+ })();
1093
+
1094
+ let updatedContent = null;
1095
+ const combinedRanges = [];
1096
+ for (const segments of providerSegmentVariants) {
1097
+ combinedRanges.push(...findProviderSectionRanges(content, BUILTIN_PROXY_PROVIDER_NAME, segments));
1098
+ combinedRanges.push(...findProviderDescendantSectionRanges(content, segments));
1099
+ }
1100
+ if (combinedRanges.length === 0) {
1101
+ combinedRanges.push(...findProviderSectionRanges(content, BUILTIN_PROXY_PROVIDER_NAME, providerSegments));
1102
+ }
1103
+
1104
+ if (combinedRanges.length > 0) {
1105
+ const sorted = combinedRanges.sort((a, b) => b.start - a.start || b.end - a.end);
1106
+ const seen = new Set();
1107
+ let removedContent = content;
1108
+ for (const range of sorted) {
1109
+ const rangeKey = `${range.start}:${range.end}`;
1110
+ if (seen.has(rangeKey)) continue;
1111
+ seen.add(rangeKey);
1112
+ removedContent = removedContent.slice(0, range.start) + removedContent.slice(range.end);
1113
+ }
1114
+ updatedContent = removedContent.replace(/\n{3,}/g, lineEnding + lineEnding);
1115
+ }
1116
+
1117
+ if (!updatedContent) {
1118
+ const rebuilt = JSON.parse(JSON.stringify(config));
1119
+ delete rebuilt.model_providers[BUILTIN_PROXY_PROVIDER_NAME];
1120
+ const hasMarker = content.includes(CODEXMATE_MANAGED_MARKER);
1121
+ let rebuiltToml = toml.stringify(rebuilt).trimEnd();
1122
+ rebuiltToml = rebuiltToml.replace(/\n/g, lineEnding);
1123
+ if (hasMarker && !rebuiltToml.includes(CODEXMATE_MANAGED_MARKER)) {
1124
+ rebuiltToml = `${CODEXMATE_MANAGED_MARKER}${lineEnding}${rebuiltToml}`;
1125
+ }
1126
+ updatedContent = rebuiltToml + lineEnding;
1127
+ if (hasBom && updatedContent.charCodeAt(0) !== 0xFEFF) {
1128
+ updatedContent = '\uFEFF' + updatedContent;
1129
+ }
1130
+ }
1131
+
1132
+ try {
1133
+ writeConfig(updatedContent.trimEnd() + lineEnding);
1134
+ } catch (e) {
1135
+ return { error: e.message || '写入 config.toml 失败' };
1136
+ }
1137
+
1138
+ return { success: true, removed: true };
1139
+ }
1140
+
1141
+ function hasCodexConfigReadyForProxy() {
1142
+ const result = readConfigOrVirtualDefault();
1143
+ if (!result || result.isVirtual) {
1144
+ return false;
1145
+ }
1146
+ const config = result.config || {};
1147
+ if (!isPlainObject(config.model_providers)) {
1148
+ return false;
1149
+ }
1150
+ const providerNames = Object.keys(config.model_providers)
1151
+ .filter((name) => name && !isBuiltinManagedProvider(name));
1152
+ return providerNames.length > 0;
1153
+ }
1154
+
1155
+ function resolveBuiltinProxyUpstream(settings) {
1156
+ const { config } = readConfigOrVirtualDefault();
1157
+ const providers = config && isPlainObject(config.model_providers) ? config.model_providers : {};
1158
+ const currentProvider = typeof config.model_provider === 'string' ? config.model_provider.trim() : '';
1159
+ const providerName = resolveBuiltinProxyProviderName(settings.provider, providers, currentProvider);
1160
+ if (!providerName) {
1161
+ return { error: '未找到可用的上游 provider,请先添加 provider' };
1162
+ }
1163
+ if (providerName === BUILTIN_PROXY_PROVIDER_NAME) {
1164
+ return { error: `上游 provider 不能是 ${BUILTIN_PROXY_PROVIDER_NAME}` };
1165
+ }
1166
+ const provider = providers[providerName];
1167
+ if (!provider || !isPlainObject(provider)) {
1168
+ return { error: `上游 provider 不存在: ${providerName}` };
1169
+ }
1170
+
1171
+ const baseUrl = typeof provider.base_url === 'string' ? provider.base_url.trim() : '';
1172
+ if (!baseUrl || !isValidHttpUrl(baseUrl)) {
1173
+ return { error: `上游 provider base_url 无效: ${providerName}` };
1174
+ }
1175
+
1176
+ let token = '';
1177
+ if (settings.authSource === 'profile') {
1178
+ token = resolveAuthTokenFromCurrentProfile();
1179
+ } else if (settings.authSource === 'provider') {
1180
+ token = typeof provider.preferred_auth_method === 'string' ? provider.preferred_auth_method.trim() : '';
1181
+ if (!token) {
1182
+ token = resolveAuthTokenFromCurrentProfile();
1183
+ }
1184
+ }
1185
+
1186
+ let authHeader = '';
1187
+ if (token) {
1188
+ authHeader = /^bearer\s+/i.test(token) ? token : `Bearer ${token}`;
1189
+ }
1190
+
1191
+ return {
1192
+ providerName,
1193
+ baseUrl: normalizeBaseUrl(baseUrl),
1194
+ authHeader
1195
+ };
1196
+ }
1197
+
1198
+ function createBuiltinProxyServer(settings, upstream) {
1199
+ const connections = new Set();
1200
+ const timeoutMs = settings.timeoutMs;
1201
+
1202
+ const server = http.createServer((req, res) => {
1203
+ let parsedIncoming;
1204
+ try {
1205
+ parsedIncoming = new URL(req.url || '/', 'http://localhost');
1206
+ } catch (e) {
1207
+ res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
1208
+ res.end(JSON.stringify({ error: 'invalid request path' }));
1209
+ return;
1210
+ }
1211
+
1212
+ const remoteAddr = req && req.socket ? req.socket.remoteAddress : '';
1213
+ const isLoopback = !remoteAddr
1214
+ || remoteAddr === '127.0.0.1'
1215
+ || remoteAddr === '::1'
1216
+ || remoteAddr === '::ffff:127.0.0.1';
1217
+ if (!isLoopback) {
1218
+ const expected = typeof process.env.CODEXMATE_HTTP_TOKEN === 'string'
1219
+ ? process.env.CODEXMATE_HTTP_TOKEN.trim()
1220
+ : '';
1221
+ if (!expected) {
1222
+ const body = JSON.stringify({ error: 'Remote access is disabled (set CODEXMATE_HTTP_TOKEN)' });
1223
+ res.writeHead(403, {
1224
+ 'Content-Type': 'application/json; charset=utf-8',
1225
+ 'Content-Length': Buffer.byteLength(body, 'utf-8')
1226
+ });
1227
+ res.end(body, 'utf-8');
1228
+ return;
1229
+ }
1230
+ const headers = req && req.headers && typeof req.headers === 'object' ? req.headers : {};
1231
+ const rawAuth = typeof headers.authorization === 'string' ? headers.authorization.trim() : '';
1232
+ const match = rawAuth ? rawAuth.match(/^bearer\s+(.+)$/i) : null;
1233
+ const actual = match && match[1]
1234
+ ? match[1].trim()
1235
+ : (rawAuth ? rawAuth : (typeof headers['x-codexmate-token'] === 'string' ? String(headers['x-codexmate-token']).trim() : ''));
1236
+ if (!actual || actual !== expected) {
1237
+ const body = JSON.stringify({ error: 'Unauthorized' });
1238
+ res.writeHead(401, {
1239
+ 'Content-Type': 'application/json; charset=utf-8',
1240
+ 'Content-Length': Buffer.byteLength(body, 'utf-8')
1241
+ });
1242
+ res.end(body, 'utf-8');
1243
+ return;
1244
+ }
1245
+ }
1246
+
1247
+ const incomingPath = parsedIncoming.pathname || '/';
1248
+ if (incomingPath === '/health' || incomingPath === '/status') {
1249
+ const body = JSON.stringify({
1250
+ ok: true,
1251
+ upstreamProvider: upstream.providerName,
1252
+ upstreamBaseUrl: upstream.baseUrl
1253
+ });
1254
+ res.writeHead(200, {
1255
+ 'Content-Type': 'application/json; charset=utf-8',
1256
+ 'Content-Length': Buffer.byteLength(body, 'utf-8')
1257
+ });
1258
+ res.end(body, 'utf-8');
1259
+ return;
1260
+ }
1261
+
1262
+ if (!(incomingPath === '/v1' || incomingPath.startsWith('/v1/'))) {
1263
+ const body = JSON.stringify({ error: 'proxy only supports /v1/* paths' });
1264
+ res.writeHead(404, {
1265
+ 'Content-Type': 'application/json; charset=utf-8',
1266
+ 'Content-Length': Buffer.byteLength(body, 'utf-8')
1267
+ });
1268
+ res.end(body, 'utf-8');
1269
+ return;
1270
+ }
1271
+
1272
+ // Responses shim:
1273
+ // - Codex CLI 默认走 /v1/responses(含 SSE)
1274
+ // - 某些上游只支持 /v1/chat/completions
1275
+ // 因此这里优先尝试 /v1/responses(stream=false),失败再转换到 chat/completions 并回包为 responses。
1276
+ if ((incomingPath === '/v1/responses' || incomingPath === '/v1/responses/') && (req.method || 'GET').toUpperCase() === 'POST') {
1277
+ void (async () => {
1278
+ const { body, error } = await readRequestBody(req, 10 * 1024 * 1024);
1279
+ if (error) {
1280
+ res.writeHead(413, { 'Content-Type': 'application/json; charset=utf-8' });
1281
+ res.end(JSON.stringify({ error }));
1282
+ return;
1283
+ }
1284
+ const parsed = parseJsonOrError(body);
1285
+ if (parsed.error) {
1286
+ res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
1287
+ res.end(JSON.stringify({ error: `invalid json: ${parsed.error}` }));
1288
+ return;
1289
+ }
1290
+
1291
+ const payload = parsed.value && typeof parsed.value === 'object' ? parsed.value : {};
1292
+ const wantsStream = payload.stream === true;
1293
+
1294
+ const commonHeaders = {
1295
+ ...(upstream.authHeader ? { 'Authorization': upstream.authHeader } : {}),
1296
+ 'X-Codexmate-Proxy': '1'
1297
+ };
1298
+
1299
+ const upstreamResponses = await proxyRequestJsonWithFallbackUrls(upstream.baseUrl, 'responses', {
1300
+ method: 'POST',
1301
+ headers: commonHeaders,
1302
+ timeoutMs,
1303
+ body: { ...payload, stream: false }
1304
+ });
1305
+
1306
+ // 优先走上游 /responses(如果支持)。若上游报错且不是“端点不支持”,则直接透传错误。
1307
+ if (upstreamResponses.ok && upstreamResponses.status >= 200 && upstreamResponses.status < 300) {
1308
+ const json = parseJsonOrError(upstreamResponses.bodyText);
1309
+ if (json.error) {
1310
+ res.writeHead(502, { 'Content-Type': 'application/json; charset=utf-8' });
1311
+ res.end(JSON.stringify({ error: `Upstream JSON parse failed: ${json.error}` }));
1312
+ return;
1313
+ }
1314
+ const responsesPayload = ensureResponseMetadata(json.value);
1315
+ if (wantsStream) {
1316
+ res.writeHead(200, {
1317
+ 'Content-Type': 'text/event-stream; charset=utf-8',
1318
+ 'Cache-Control': 'no-cache',
1319
+ 'Connection': 'keep-alive',
1320
+ 'X-Accel-Buffering': 'no'
1321
+ });
1322
+ sendResponsesSse(res, responsesPayload);
1323
+ res.end();
1324
+ return;
1325
+ }
1326
+ res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
1327
+ res.end(JSON.stringify(responsesPayload));
1328
+ return;
1329
+ }
1330
+
1331
+ if (upstreamResponses.ok && upstreamResponses.status >= 400) {
1332
+ if (!shouldFallbackFromUpstreamResponses(upstreamResponses.status, upstreamResponses.bodyText)) {
1333
+ res.writeHead(upstreamResponses.status, { 'Content-Type': 'application/json; charset=utf-8' });
1334
+ res.end(upstreamResponses.bodyText || JSON.stringify({ error: 'Upstream error' }));
1335
+ return;
1336
+ }
1337
+ // fallthrough to chat/completions conversion
1338
+ }
1339
+
1340
+ if (!upstreamResponses.ok) {
1341
+ if (!shouldFallbackFromUpstreamResponsesFailure(upstreamResponses.error)) {
1342
+ res.writeHead(502, { 'Content-Type': 'application/json; charset=utf-8' });
1343
+ res.end(JSON.stringify({ error: upstreamResponses.error || 'Upstream request failed' }));
1344
+ return;
1345
+ }
1346
+ // Some OpenAI-compatible gateways accept /responses but never complete it.
1347
+ // Treat that as an unsupported Responses endpoint and try the chat fallback.
1348
+ }
1349
+
1350
+ const model = typeof payload.model === 'string' ? payload.model : '';
1351
+ const chatBody = buildChatCompletionsBodyFromResponsesPayload(payload);
1352
+
1353
+ if (wantsStream) {
1354
+ const streamingChatBody = { ...chatBody, stream: true };
1355
+ const streamed = await streamChatCompletionsAsResponsesSseWithFallbackUrls(upstream.baseUrl, 'chat/completions', {
1356
+ method: 'POST',
1357
+ headers: commonHeaders,
1358
+ timeoutMs,
1359
+ body: streamingChatBody,
1360
+ res,
1361
+ model
1362
+ });
1363
+ if (!streamed.ok) {
1364
+ if (!res.headersSent) {
1365
+ res.writeHead(streamed.status && streamed.status >= 400 ? streamed.status : 502, { 'Content-Type': 'application/json; charset=utf-8' });
1366
+ res.end(streamed.bodyText || JSON.stringify({ error: streamed.error || 'proxy request failed' }));
1367
+ } else if (!res.writableEnded) {
1368
+ writeSse(res, 'response.failed', { type: 'response.failed', error: streamed.error || streamed.bodyText || 'proxy request failed' });
1369
+ writeSse(res, 'done', '[DONE]');
1370
+ res.end();
1371
+ }
1372
+ }
1373
+ return;
1374
+ }
1375
+
1376
+ const upstreamChat = await proxyRequestJsonWithFallbackUrls(upstream.baseUrl, 'chat/completions', {
1377
+ method: 'POST',
1378
+ headers: commonHeaders,
1379
+ timeoutMs,
1380
+ body: chatBody
1381
+ });
1382
+ if (!upstreamChat.ok) {
1383
+ res.writeHead(502, { 'Content-Type': 'application/json; charset=utf-8' });
1384
+ res.end(JSON.stringify({ error: upstreamChat.error || 'proxy request failed' }));
1385
+ return;
1386
+ }
1387
+
1388
+ if (upstreamChat.status >= 400) {
1389
+ res.writeHead(upstreamChat.status, { 'Content-Type': 'application/json; charset=utf-8' });
1390
+ res.end(upstreamChat.bodyText || JSON.stringify({ error: 'Upstream error' }));
1391
+ return;
1392
+ }
1393
+
1394
+ const chatJson = parseJsonOrError(upstreamChat.bodyText);
1395
+ if (chatJson.error) {
1396
+ res.writeHead(502, { 'Content-Type': 'application/json; charset=utf-8' });
1397
+ res.end(JSON.stringify({ error: `invalid upstream response: ${chatJson.error}` }));
1398
+ return;
1399
+ }
1400
+
1401
+ const responsesPayload = buildResponsesPayloadFromChatCompletion(chatJson.value, model);
1402
+
1403
+ if (wantsStream) {
1404
+ res.writeHead(200, {
1405
+ 'Content-Type': 'text/event-stream; charset=utf-8',
1406
+ 'Cache-Control': 'no-cache',
1407
+ 'Connection': 'keep-alive',
1408
+ 'X-Accel-Buffering': 'no'
1409
+ });
1410
+ sendResponsesSse(res, responsesPayload);
1411
+ res.end();
1412
+ return;
1413
+ }
1414
+ res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
1415
+ res.end(JSON.stringify(responsesPayload));
1416
+ })();
1417
+ return;
1418
+ }
1419
+
1420
+ const suffix = incomingPath === '/v1'
1421
+ ? ''
1422
+ : incomingPath.replace(/^\/v1\/?/, '');
1423
+ const targetBase = joinApiUrl(upstream.baseUrl, suffix);
1424
+ if (!targetBase) {
1425
+ const body = JSON.stringify({ error: 'failed to build upstream URL' });
1426
+ res.writeHead(500, {
1427
+ 'Content-Type': 'application/json; charset=utf-8',
1428
+ 'Content-Length': Buffer.byteLength(body, 'utf-8')
1429
+ });
1430
+ res.end(body, 'utf-8');
1431
+ return;
1432
+ }
1433
+
1434
+ let targetUrl;
1435
+ try {
1436
+ targetUrl = new URL(targetBase);
1437
+ targetUrl.search = parsedIncoming.search || '';
1438
+ } catch (e) {
1439
+ const body = JSON.stringify({ error: `invalid upstream URL: ${e.message}` });
1440
+ res.writeHead(500, {
1441
+ 'Content-Type': 'application/json; charset=utf-8',
1442
+ 'Content-Length': Buffer.byteLength(body, 'utf-8')
1443
+ });
1444
+ res.end(body, 'utf-8');
1445
+ return;
1446
+ }
1447
+
1448
+ const requestHeaders = { ...req.headers };
1449
+ delete requestHeaders.host;
1450
+ delete requestHeaders.connection;
1451
+ delete requestHeaders['content-length'];
1452
+ if (upstream.authHeader) {
1453
+ requestHeaders.authorization = upstream.authHeader;
1454
+ }
1455
+ requestHeaders['x-codexmate-proxy'] = '1';
1456
+ if (!requestHeaders['x-forwarded-for'] && req.socket && req.socket.remoteAddress) {
1457
+ requestHeaders['x-forwarded-for'] = req.socket.remoteAddress;
1458
+ }
1459
+
1460
+ const transport = targetUrl.protocol === 'https:' ? https : http;
1461
+ const upstreamReq = transport.request({
1462
+ protocol: targetUrl.protocol,
1463
+ hostname: targetUrl.hostname,
1464
+ port: targetUrl.port || (targetUrl.protocol === 'https:' ? 443 : 80),
1465
+ method: req.method || 'GET',
1466
+ path: `${targetUrl.pathname}${targetUrl.search}`,
1467
+ headers: requestHeaders,
1468
+ agent: targetUrl.protocol === 'https:' ? HTTPS_KEEP_ALIVE_AGENT : HTTP_KEEP_ALIVE_AGENT
1469
+ }, (upstreamRes) => {
1470
+ const responseHeaders = { ...upstreamRes.headers };
1471
+ delete responseHeaders.connection;
1472
+ res.writeHead(upstreamRes.statusCode || 502, responseHeaders);
1473
+ upstreamRes.pipe(res);
1474
+ });
1475
+
1476
+ upstreamReq.setTimeout(timeoutMs, () => {
1477
+ upstreamReq.destroy(new Error(`upstream timeout (${timeoutMs}ms)`));
1478
+ });
1479
+
1480
+ upstreamReq.on('error', (err) => {
1481
+ if (res.headersSent) {
1482
+ try { res.destroy(err); } catch (_) {}
1483
+ return;
1484
+ }
1485
+ const body = JSON.stringify({ error: `proxy request failed: ${err.message}` });
1486
+ res.writeHead(502, {
1487
+ 'Content-Type': 'application/json; charset=utf-8',
1488
+ 'Content-Length': Buffer.byteLength(body, 'utf-8')
1489
+ });
1490
+ res.end(body, 'utf-8');
1491
+ });
1492
+
1493
+ req.pipe(upstreamReq);
1494
+ });
1495
+
1496
+ server.on('connection', (socket) => {
1497
+ connections.add(socket);
1498
+ socket.on('close', () => connections.delete(socket));
1499
+ });
1500
+
1501
+ return new Promise((resolve, reject) => {
1502
+ server.once('error', reject);
1503
+ server.listen(settings.port, settings.host, () => {
1504
+ server.removeListener('error', reject);
1505
+ resolve({
1506
+ server,
1507
+ connections,
1508
+ settings,
1509
+ upstream,
1510
+ startedAt: toIsoTime(Date.now()),
1511
+ listenUrl: buildProxyListenUrl(settings)
1512
+ });
1513
+ });
1514
+ });
1515
+ }
1516
+
1517
+ async function startBuiltinProxyRuntime(payload = {}) {
1518
+ if (runtime) {
1519
+ return {
1520
+ error: '内建代理已在运行',
1521
+ runtime: {
1522
+ listenUrl: runtime.listenUrl,
1523
+ upstreamProvider: runtime.upstream.providerName
1524
+ }
1525
+ };
1526
+ }
1527
+
1528
+ const saveResult = saveBuiltinProxySettings(payload);
1529
+ if (saveResult.error) {
1530
+ return { error: saveResult.error };
1531
+ }
1532
+ const settings = saveResult.settings;
1533
+ const upstream = resolveBuiltinProxyUpstream(settings);
1534
+ if (upstream.error) {
1535
+ return { error: upstream.error };
1536
+ }
1537
+
1538
+ try {
1539
+ runtime = await createBuiltinProxyServer(settings, upstream);
1540
+ return {
1541
+ success: true,
1542
+ running: true,
1543
+ listenUrl: runtime.listenUrl,
1544
+ upstreamProvider: upstream.providerName,
1545
+ settings
1546
+ };
1547
+ } catch (e) {
1548
+ return { error: `启动内建代理失败: ${e.message}` };
1549
+ }
1550
+ }
1551
+
1552
+ async function stopBuiltinProxyRuntime() {
1553
+ if (!runtime) {
1554
+ return { success: true, running: false };
1555
+ }
1556
+ const currentRuntime = runtime;
1557
+ runtime = null;
1558
+
1559
+ await new Promise((resolve) => {
1560
+ let settled = false;
1561
+ const finish = () => {
1562
+ if (settled) return;
1563
+ settled = true;
1564
+ resolve();
1565
+ };
1566
+
1567
+ currentRuntime.server.close(() => finish());
1568
+ setTimeout(() => finish(), 1000);
1569
+ });
1570
+
1571
+ for (const socket of currentRuntime.connections) {
1572
+ try { socket.destroy(); } catch (_) {}
1573
+ }
1574
+ currentRuntime.connections.clear();
1575
+
1576
+ return {
1577
+ success: true,
1578
+ running: false
1579
+ };
1580
+ }
1581
+
1582
+ function getBuiltinProxyStatus() {
1583
+ const settings = readBuiltinProxySettings();
1584
+ return {
1585
+ running: !!runtime,
1586
+ settings,
1587
+ runtime: runtime
1588
+ ? {
1589
+ provider: BUILTIN_PROXY_PROVIDER_NAME,
1590
+ startedAt: runtime.startedAt,
1591
+ listenUrl: runtime.listenUrl,
1592
+ upstreamProvider: runtime.upstream.providerName,
1593
+ upstreamBaseUrl: runtime.upstream.baseUrl
1594
+ }
1595
+ : null
1596
+ };
1597
+ }
1598
+
1599
+ return {
1600
+ canListenPort,
1601
+ findAvailablePort,
1602
+ normalizeBuiltinProxySettings,
1603
+ readBuiltinProxySettings,
1604
+ resolveBuiltinProxyProviderName,
1605
+ saveBuiltinProxySettings,
1606
+ buildProxyListenUrl,
1607
+ buildBuiltinProxyProviderBaseUrl,
1608
+ removePersistedBuiltinProxyProviderFromConfig,
1609
+ hasCodexConfigReadyForProxy,
1610
+ resolveBuiltinProxyUpstream,
1611
+ createBuiltinProxyServer,
1612
+ startBuiltinProxyRuntime,
1613
+ stopBuiltinProxyRuntime,
1614
+ getBuiltinProxyStatus
1615
+ };
1616
+ }
1617
+
1618
+ module.exports = {
1619
+ createBuiltinProxyRuntimeController
1620
+ };