codexmate 0.0.40 → 0.0.41

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 (155) hide show
  1. package/README.md +168 -156
  2. package/README.zh.md +168 -156
  3. package/cli/agents-files.js +230 -230
  4. package/cli/analytics-export-args.js +68 -68
  5. package/cli/archive-helpers.js +453 -453
  6. package/cli/auth-profiles.js +375 -375
  7. package/cli/builtin-proxy.js +2144 -2144
  8. package/cli/claude-proxy.js +1022 -1022
  9. package/cli/config-bootstrap.js +407 -407
  10. package/cli/config-health.js +454 -454
  11. package/cli/doctor-core.js +903 -903
  12. package/cli/import-skills-url.js +356 -356
  13. package/cli/local-bridge.js +556 -556
  14. package/cli/openai-bridge.js +1984 -1984
  15. package/cli/openclaw-config.js +629 -629
  16. package/cli/session-convert-args.js +69 -69
  17. package/cli/session-convert-io.js +82 -82
  18. package/cli/session-convert.js +150 -150
  19. package/cli/session-usage.concurrent.js +28 -28
  20. package/cli/session-usage.js +304 -304
  21. package/cli/session-usage.models.js +176 -176
  22. package/cli/skills.js +1141 -1141
  23. package/cli/update.js +171 -171
  24. package/cli/zip-commands.js +510 -510
  25. package/cli.js +16458 -16341
  26. package/lib/automation.js +404 -404
  27. package/lib/cli-file-utils.js +151 -151
  28. package/lib/cli-models-utils.js +440 -440
  29. package/lib/cli-network-utils.js +190 -190
  30. package/lib/cli-path-utils.js +85 -85
  31. package/lib/cli-session-utils.js +121 -121
  32. package/lib/cli-sessions.js +427 -427
  33. package/lib/cli-utils.js +155 -155
  34. package/lib/cli-webhook.js +154 -154
  35. package/lib/download-artifacts.js +92 -92
  36. package/lib/mcp-stdio.js +453 -453
  37. package/lib/task-orchestrator.js +869 -869
  38. package/lib/text-diff.js +303 -303
  39. package/lib/win-tray.js +119 -119
  40. package/lib/workflow-engine.js +340 -340
  41. package/package.json +77 -77
  42. package/plugins/README.md +20 -20
  43. package/plugins/README.zh-CN.md +20 -20
  44. package/plugins/prompt-templates/comment-polish/index.mjs +25 -25
  45. package/plugins/prompt-templates/computed.mjs +311 -311
  46. package/plugins/prompt-templates/index.mjs +8 -8
  47. package/plugins/prompt-templates/manifest.mjs +18 -18
  48. package/plugins/prompt-templates/methods.mjs +553 -553
  49. package/plugins/prompt-templates/overview.mjs +91 -91
  50. package/plugins/prompt-templates/ownership.mjs +19 -19
  51. package/plugins/prompt-templates/rule-ack/index.mjs +21 -21
  52. package/plugins/prompt-templates/storage.mjs +64 -64
  53. package/plugins/registry.mjs +16 -16
  54. package/web-ui/app.js +695 -695
  55. package/web-ui/index.html +37 -37
  56. package/web-ui/logic.agents-diff.mjs +386 -386
  57. package/web-ui/logic.claude.mjs +172 -172
  58. package/web-ui/logic.codex.mjs +69 -69
  59. package/web-ui/logic.mjs +5 -5
  60. package/web-ui/logic.runtime.mjs +128 -128
  61. package/web-ui/logic.session-convert.mjs +70 -70
  62. package/web-ui/logic.sessions.mjs +782 -782
  63. package/web-ui/modules/api.mjs +90 -90
  64. package/web-ui/modules/app.computed.dashboard.mjs +252 -252
  65. package/web-ui/modules/app.computed.index.mjs +17 -17
  66. package/web-ui/modules/app.computed.main-tabs.mjs +214 -214
  67. package/web-ui/modules/app.computed.session.mjs +876 -876
  68. package/web-ui/modules/app.constants.mjs +15 -15
  69. package/web-ui/modules/app.methods.agents.mjs +651 -651
  70. package/web-ui/modules/app.methods.claude-config.mjs +412 -412
  71. package/web-ui/modules/app.methods.codex-config.mjs +869 -869
  72. package/web-ui/modules/app.methods.index.mjs +96 -96
  73. package/web-ui/modules/app.methods.install.mjs +205 -205
  74. package/web-ui/modules/app.methods.navigation.mjs +804 -804
  75. package/web-ui/modules/app.methods.openclaw-core.mjs +814 -814
  76. package/web-ui/modules/app.methods.openclaw-editing.mjs +420 -420
  77. package/web-ui/modules/app.methods.openclaw-persist.mjs +375 -375
  78. package/web-ui/modules/app.methods.providers.mjs +601 -601
  79. package/web-ui/modules/app.methods.runtime.mjs +420 -420
  80. package/web-ui/modules/app.methods.session-actions.mjs +591 -591
  81. package/web-ui/modules/app.methods.session-browser.mjs +1018 -1018
  82. package/web-ui/modules/app.methods.session-timeline.mjs +479 -479
  83. package/web-ui/modules/app.methods.session-trash.mjs +468 -468
  84. package/web-ui/modules/app.methods.startup-claude.mjs +554 -554
  85. package/web-ui/modules/app.methods.task-orchestration.mjs +556 -556
  86. package/web-ui/modules/app.methods.tool-config-permissions.mjs +87 -87
  87. package/web-ui/modules/app.methods.webhook.mjs +87 -87
  88. package/web-ui/modules/config-mode.computed.mjs +124 -124
  89. package/web-ui/modules/config-template-confirm-pref.mjs +33 -33
  90. package/web-ui/modules/i18n/locales/en.mjs +1140 -1140
  91. package/web-ui/modules/i18n/locales/ja.mjs +1130 -1130
  92. package/web-ui/modules/i18n/locales/vi.mjs +239 -239
  93. package/web-ui/modules/i18n/locales/zh.mjs +1143 -1143
  94. package/web-ui/modules/i18n.dict.mjs +14 -14
  95. package/web-ui/modules/i18n.mjs +111 -111
  96. package/web-ui/modules/plugins.computed.mjs +3 -3
  97. package/web-ui/modules/plugins.methods.mjs +3 -3
  98. package/web-ui/modules/plugins.storage.mjs +11 -11
  99. package/web-ui/modules/provider-url-display.mjs +17 -17
  100. package/web-ui/modules/sessions-filters-url.mjs +138 -138
  101. package/web-ui/modules/skills.computed.mjs +107 -107
  102. package/web-ui/modules/skills.methods.mjs +513 -513
  103. package/web-ui/partials/index/layout-footer.html +13 -13
  104. package/web-ui/partials/index/layout-header.html +478 -478
  105. package/web-ui/partials/index/modal-config-template-agents.html +185 -185
  106. package/web-ui/partials/index/modal-confirm-toast.html +32 -32
  107. package/web-ui/partials/index/modal-health-check.html +45 -45
  108. package/web-ui/partials/index/modal-openclaw-config.html +344 -344
  109. package/web-ui/partials/index/modal-skills.html +200 -200
  110. package/web-ui/partials/index/modal-webhook.html +42 -42
  111. package/web-ui/partials/index/modals-basic.html +263 -263
  112. package/web-ui/partials/index/panel-config-claude.html +187 -187
  113. package/web-ui/partials/index/panel-config-codex.html +205 -205
  114. package/web-ui/partials/index/panel-config-openclaw.html +89 -89
  115. package/web-ui/partials/index/panel-dashboard.html +171 -171
  116. package/web-ui/partials/index/panel-docs.html +114 -114
  117. package/web-ui/partials/index/panel-market.html +104 -104
  118. package/web-ui/partials/index/panel-orchestration.html +391 -391
  119. package/web-ui/partials/index/panel-plugins.html +253 -253
  120. package/web-ui/partials/index/panel-sessions.html +319 -319
  121. package/web-ui/partials/index/panel-settings.html +181 -181
  122. package/web-ui/partials/index/panel-trash.html +82 -82
  123. package/web-ui/partials/index/panel-usage.html +181 -181
  124. package/web-ui/res/json5.min.js +1 -1
  125. package/web-ui/res/vue.global.prod.js +13 -13
  126. package/web-ui/res/vue.runtime.global.prod.js +7 -7
  127. package/web-ui/res/web-ui-render.precompiled.js +7666 -7666
  128. package/web-ui/session-helpers.mjs +602 -602
  129. package/web-ui/source-bundle.cjs +305 -305
  130. package/web-ui/styles/base-theme.css +291 -291
  131. package/web-ui/styles/bridge-pool.css +266 -266
  132. package/web-ui/styles/controls-forms.css +532 -532
  133. package/web-ui/styles/dashboard.css +438 -438
  134. package/web-ui/styles/docs-panel.css +245 -245
  135. package/web-ui/styles/feedback.css +108 -108
  136. package/web-ui/styles/health-check-dialog.css +144 -144
  137. package/web-ui/styles/layout-shell.css +711 -711
  138. package/web-ui/styles/modals-core.css +499 -499
  139. package/web-ui/styles/navigation-panels.css +399 -399
  140. package/web-ui/styles/openclaw-structured.css +616 -616
  141. package/web-ui/styles/plugins-panel.css +564 -564
  142. package/web-ui/styles/responsive.css +501 -501
  143. package/web-ui/styles/sessions-list.css +683 -683
  144. package/web-ui/styles/sessions-preview.css +407 -407
  145. package/web-ui/styles/sessions-toolbar-trash.css +518 -518
  146. package/web-ui/styles/sessions-usage.css +849 -849
  147. package/web-ui/styles/settings-panel.css +419 -419
  148. package/web-ui/styles/skills-list.css +305 -305
  149. package/web-ui/styles/skills-market.css +723 -723
  150. package/web-ui/styles/task-orchestration.css +822 -822
  151. package/web-ui/styles/titles-cards.css +486 -486
  152. package/web-ui/styles/trash-panel.css +90 -90
  153. package/web-ui/styles/webhook.css +115 -115
  154. package/web-ui/styles.css +24 -24
  155. package/web-ui.html +17 -17
@@ -1,2144 +1,2144 @@
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 isTransientNetworkError(error) {
131
- const text = String(error || '').trim();
132
- if (!text) return false;
133
- if (/socket hang up/i.test(text)) return true;
134
- if (/ECONNRESET|ECONNREFUSED|EPIPE|EPROTO|ETIMEDOUT/i.test(text)) return true;
135
- if (/EAI_AGAIN/i.test(text)) return true;
136
- if (/UND_ERR_SOCKET/i.test(text)) return true;
137
- if (/disconnected before|secure tls|tls handshake/i.test(text)) return true;
138
- return false;
139
- }
140
-
141
- const TRANSIENT_RETRY_DELAYS_MS = [200, 600, 1200];
142
-
143
- async function retryTransientRequest(executor) {
144
- let lastResult = null;
145
- for (let attempt = 0; attempt <= TRANSIENT_RETRY_DELAYS_MS.length; attempt += 1) {
146
- if (attempt > 0) {
147
- const delay = TRANSIENT_RETRY_DELAYS_MS[attempt - 1];
148
- // eslint-disable-next-line no-await-in-loop
149
- await new Promise((r) => {
150
- const t = setTimeout(r, delay);
151
- if (typeof t.unref === 'function') t.unref();
152
- });
153
- }
154
- // eslint-disable-next-line no-await-in-loop
155
- const result = await executor(attempt);
156
- lastResult = result;
157
- if (!result) return result;
158
- if (result.ok) return result;
159
- if (result.retry) return result;
160
- if (result.status && result.status > 0) return result;
161
- if (!result.retryTransient && !isTransientNetworkError(result.error)) return result;
162
- }
163
- return lastResult;
164
- }
165
-
166
- function proxyRequestJson(targetUrl, options = {}) {
167
- const parsed = new URL(targetUrl);
168
- const transport = parsed.protocol === 'https:' ? https : http;
169
- const bodyText = options.body ? JSON.stringify(options.body) : '';
170
- const headers = {
171
- 'Accept': 'application/json',
172
- ...(options.body ? { 'Content-Type': 'application/json' } : {}),
173
- ...(options.headers || {})
174
- };
175
- if (options.body) {
176
- headers['Content-Length'] = Buffer.byteLength(bodyText, 'utf-8');
177
- }
178
- const timeoutMs = Number.isFinite(options.timeoutMs)
179
- ? Math.max(1000, Number(options.timeoutMs))
180
- : 30000;
181
-
182
- return new Promise((resolve) => {
183
- let settled = false;
184
- const finish = (value) => {
185
- if (settled) return;
186
- settled = true;
187
- resolve(value);
188
- };
189
- const req = transport.request({
190
- protocol: parsed.protocol,
191
- hostname: parsed.hostname,
192
- port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
193
- method: options.method || 'GET',
194
- path: `${parsed.pathname}${parsed.search}`,
195
- headers,
196
- agent: parsed.protocol === 'https:' ? HTTPS_KEEP_ALIVE_AGENT : HTTP_KEEP_ALIVE_AGENT
197
- }, (upstreamRes) => {
198
- const chunks = [];
199
- upstreamRes.on('data', (chunk) => chunk && chunks.push(chunk));
200
- upstreamRes.on('end', () => {
201
- const text = chunks.length ? Buffer.concat(chunks).toString('utf-8') : '';
202
- finish({
203
- ok: true,
204
- status: upstreamRes.statusCode || 0,
205
- headers: upstreamRes.headers || {},
206
- bodyText: text
207
- });
208
- });
209
- });
210
- req.setTimeout(timeoutMs, () => {
211
- try { req.destroy(new Error('timeout')); } catch (_) {}
212
- finish({ ok: false, error: 'timeout' });
213
- });
214
- req.on('error', (err) => finish({ ok: false, error: err && err.message ? err.message : 'request failed' }));
215
- if (bodyText) {
216
- req.write(bodyText);
217
- }
218
- req.end();
219
- });
220
- }
221
-
222
- function buildUpstreamUrlCandidates(baseUrl, pathSuffix) {
223
- const safeSuffix = String(pathSuffix || '').replace(/^\/+/, '');
224
- const candidates = [];
225
- const push = (url) => {
226
- if (url && !candidates.includes(url)) {
227
- candidates.push(url);
228
- }
229
- };
230
- push(joinApiUrl(baseUrl, safeSuffix));
231
- const trimmed = normalizeBaseUrl(baseUrl);
232
- if (trimmed && safeSuffix) {
233
- push(`${trimmed}/${safeSuffix}`);
234
- }
235
- return candidates;
236
- }
237
-
238
- async function proxyRequestJsonWithFallbackUrls(baseUrl, pathSuffix, options = {}) {
239
- const urls = buildUpstreamUrlCandidates(baseUrl, pathSuffix);
240
- if (urls.length === 0) {
241
- return { ok: false, error: 'failed to build upstream URL' };
242
- }
243
- let lastResult = null;
244
- for (let index = 0; index < urls.length; index += 1) {
245
- const result = await retryTransientRequest(() => proxyRequestJson(urls[index], options));
246
- lastResult = result;
247
- if (!result.ok) {
248
- return result;
249
- }
250
- if (!(result.status === 404 || result.status === 405)) {
251
- return result;
252
- }
253
- }
254
- return lastResult || { ok: false, error: 'failed to build upstream URL' };
255
- }
256
-
257
- function stringifyJsonValue(value, fallback = '') {
258
- if (typeof value === 'string') return value;
259
- if (value == null) return fallback;
260
- try {
261
- return JSON.stringify(value);
262
- } catch (_) {
263
- return fallback;
264
- }
265
- }
266
-
267
- function parseJsonValueOrNull(value) {
268
- if (typeof value !== 'string') return null;
269
- const text = value.trim();
270
- if (!text) return null;
271
- try {
272
- return JSON.parse(text);
273
- } catch (_) {
274
- return null;
275
- }
276
- }
277
-
278
- function normalizeChatUsageToResponsesUsage(usage) {
279
- if (!usage || typeof usage !== 'object' || Array.isArray(usage)) return undefined;
280
- const pickNumber = (...keys) => {
281
- for (const key of keys) {
282
- if (Number.isFinite(usage[key])) return usage[key];
283
- }
284
- return undefined;
285
- };
286
- const inputTokens = pickNumber('input_tokens', 'prompt_tokens');
287
- const outputTokens = pickNumber('output_tokens', 'completion_tokens');
288
- const totalTokens = pickNumber('total_tokens');
289
- const result = {};
290
- if (inputTokens != null) result.input_tokens = inputTokens;
291
- if (outputTokens != null) result.output_tokens = outputTokens;
292
- if (totalTokens != null) result.total_tokens = totalTokens;
293
- if (usage.input_tokens_details && typeof usage.input_tokens_details === 'object') {
294
- result.input_tokens_details = usage.input_tokens_details;
295
- } else if (usage.prompt_tokens_details && typeof usage.prompt_tokens_details === 'object') {
296
- result.input_tokens_details = usage.prompt_tokens_details;
297
- }
298
- if (usage.output_tokens_details && typeof usage.output_tokens_details === 'object') {
299
- result.output_tokens_details = usage.output_tokens_details;
300
- } else if (usage.completion_tokens_details && typeof usage.completion_tokens_details === 'object') {
301
- result.output_tokens_details = usage.completion_tokens_details;
302
- }
303
- return Object.keys(result).length > 0 ? result : usage;
304
- }
305
-
306
- function mapChatFinishReasonToResponses(choice) {
307
- const finishReason = choice && typeof choice === 'object' && typeof choice.finish_reason === 'string'
308
- ? choice.finish_reason
309
- : '';
310
- if (finishReason === 'length') {
311
- return { status: 'incomplete', incomplete_details: { reason: 'max_output_tokens' } };
312
- }
313
- if (finishReason === 'content_filter') {
314
- return { status: 'incomplete', incomplete_details: { reason: 'content_filter' } };
315
- }
316
- return { status: 'completed' };
317
- }
318
-
319
- function normalizeChatMessageContentToResponsesContent(content, refusal = '') {
320
- const blocks = [];
321
- const pushText = (text) => {
322
- if (typeof text === 'string' && text) {
323
- blocks.push({ type: 'output_text', text });
324
- }
325
- };
326
- if (typeof content === 'string') {
327
- pushText(content);
328
- } else if (Array.isArray(content)) {
329
- for (const item of content) {
330
- if (!item) continue;
331
- if (typeof item === 'string') {
332
- pushText(item);
333
- continue;
334
- }
335
- if (typeof item !== 'object') continue;
336
- const type = typeof item.type === 'string' ? item.type : '';
337
- if ((type === 'text' || type === 'output_text') && typeof item.text === 'string') {
338
- pushText(item.text);
339
- continue;
340
- }
341
- if (typeof item.content === 'string') {
342
- pushText(item.content);
343
- }
344
- }
345
- }
346
- if (typeof refusal === 'string' && refusal) {
347
- blocks.push({ type: 'refusal', refusal });
348
- }
349
- return blocks;
350
- }
351
-
352
- function buildResponsesPayloadFromChatCompletion(payload, fallbackModel = '', options = {}) {
353
- const base = payload && typeof payload === 'object' && !Array.isArray(payload) ? payload : {};
354
- const choice = Array.isArray(base.choices) ? base.choices[0] : null;
355
- const message = choice && typeof choice === 'object' && choice.message && typeof choice.message === 'object'
356
- ? choice.message
357
- : {};
358
- const output = [];
359
- const messageContent = normalizeChatMessageContentToResponsesContent(message.content, message.refusal);
360
- if (messageContent.length > 0 || !Array.isArray(message.tool_calls) || message.tool_calls.length === 0) {
361
- output.push({
362
- type: 'message',
363
- role: 'assistant',
364
- content: messageContent.length > 0 ? messageContent : [{ type: 'output_text', text: '' }]
365
- });
366
- }
367
- if (Array.isArray(message.tool_calls)) {
368
- for (const toolCall of message.tool_calls) {
369
- const item = buildResponsesToolCallItemFromChatToolCall(toolCall, options.toolTypesByName || {});
370
- if (item) output.push(item);
371
- }
372
- }
373
- const finish = mapChatFinishReasonToResponses(choice);
374
- return ensureResponseMetadata({
375
- id: typeof base.id === 'string' ? base.id : undefined,
376
- model: typeof base.model === 'string' ? base.model : fallbackModel,
377
- status: finish.status,
378
- ...(finish.incomplete_details ? { incomplete_details: finish.incomplete_details } : {}),
379
- output,
380
- usage: normalizeChatUsageToResponsesUsage(base.usage)
381
- });
382
- }
383
-
384
- function isRecord(value) {
385
- return !!value && typeof value === 'object' && !Array.isArray(value);
386
- }
387
-
388
- function asTrimmedString(value) {
389
- return typeof value === 'string' ? value.trim() : '';
390
- }
391
-
392
- function cloneJsonValue(value) {
393
- if (Array.isArray(value)) return value.map((item) => cloneJsonValue(item));
394
- if (isRecord(value)) {
395
- return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, cloneJsonValue(item)]));
396
- }
397
- return value;
398
- }
399
-
400
- function normalizeResponsesToolOutput(value) {
401
- if (typeof value === 'string') return value;
402
- if (value == null) return '';
403
- return stringifyJsonValue(value, '');
404
- }
405
-
406
- function normalizeOpenAiToolArguments(value) {
407
- if (typeof value === 'string') return value;
408
- if (value == null) return '{}';
409
- return stringifyJsonValue(value, '{}');
410
- }
411
-
412
- function normalizeInputFileBlock(item) {
413
- if (!isRecord(item)) return null;
414
- const file = isRecord(item.file) ? item.file : item;
415
- const out = {};
416
- const fileId = asTrimmedString(file.file_id || file.id);
417
- const filename = asTrimmedString(file.filename || file.name);
418
- const fileData = asTrimmedString(file.file_data || file.data);
419
- const mimeType = asTrimmedString(file.mime_type || file.media_type);
420
- if (fileId) out.file_id = fileId;
421
- if (filename) out.filename = filename;
422
- if (fileData) out.file_data = fileData;
423
- if (mimeType) out.mime_type = mimeType;
424
- return Object.keys(out).length > 0 ? out : null;
425
- }
426
-
427
- function normalizeResponsesContentBlockForChat(item) {
428
- if (typeof item === 'string') return item.trim() ? item : null;
429
- if (!isRecord(item)) return null;
430
-
431
- const type = asTrimmedString(item.type).toLowerCase();
432
- if (!type) {
433
- const text = asTrimmedString(item.text || item.content || item.output_text);
434
- return text ? { type: 'text', text } : null;
435
- }
436
-
437
- if (type === 'input_text' || type === 'output_text' || type === 'text' || type === 'summary_text' || type === 'reasoning_text') {
438
- const text = typeof item.text === 'string' ? item.text : asTrimmedString(item.content || item.output_text);
439
- return text ? { type: 'text', text } : null;
440
- }
441
-
442
- if (type === 'refusal' && typeof item.refusal === 'string') {
443
- return item.refusal ? { type: 'text', text: item.refusal } : null;
444
- }
445
-
446
- if (type === 'input_image') {
447
- const raw = item.image_url != null ? item.image_url : (item.url != null ? item.url : item.imageUrl);
448
- if (raw === undefined) return null;
449
- return {
450
- type: 'image_url',
451
- image_url: typeof raw === 'string' ? { url: raw } : cloneJsonValue(raw)
452
- };
453
- }
454
-
455
- if (type === 'image_url' && item.image_url !== undefined) {
456
- return { type: 'image_url', image_url: item.image_url };
457
- }
458
-
459
- if (type === 'input_audio') {
460
- if (item.input_audio !== undefined) return { type: 'input_audio', input_audio: item.input_audio };
461
- if (item.data !== undefined || item.format !== undefined) {
462
- return { type: 'input_audio', input_audio: { data: item.data, format: item.format } };
463
- }
464
- return null;
465
- }
466
-
467
- if (type === 'input_file' || type === 'file') {
468
- const file = normalizeInputFileBlock(item);
469
- return file ? { type: 'file', file } : null;
470
- }
471
-
472
- if (type === 'reasoning' || type === 'thinking' || type === 'redacted_reasoning') {
473
- const text = asTrimmedString(item.text || item.content);
474
- return text ? { type: 'text', text } : null;
475
- }
476
-
477
- return cloneJsonValue(item);
478
- }
479
-
480
- function toOpenAiMessageContent(content) {
481
- if (typeof content === 'string') return content;
482
- if (!Array.isArray(content)) {
483
- if (isRecord(content)) {
484
- const single = normalizeResponsesContentBlockForChat(content);
485
- if (!single) return '';
486
- return typeof single === 'string' ? single : [single];
487
- }
488
- return '';
489
- }
490
-
491
- const blocks = content
492
- .map((item) => normalizeResponsesContentBlockForChat(item))
493
- .filter((item) => !!item);
494
-
495
- if (blocks.length === 0) return '';
496
- if (blocks.length === 1 && typeof blocks[0] === 'string') return blocks[0];
497
- return blocks;
498
- }
499
-
500
- const RESPONSES_TOOL_CALL_INPUT_TYPES = new Set(['function_call', 'custom_tool_call', 'mcp_tool_call', 'local_shell_call']);
501
- const RESPONSES_TOOL_CALL_OUTPUT_TYPES = new Set(['function_call_output', 'custom_tool_call_output', 'mcp_tool_call_output', 'tool_search_output', 'local_shell_call_output']);
502
-
503
- function stripOrphanedResponsesToolOutputs(input) {
504
- if (!Array.isArray(input)) return input;
505
- const seenToolCallIds = new Set();
506
- const sanitized = [];
507
- for (const item of input) {
508
- if (!isRecord(item)) {
509
- sanitized.push(item);
510
- continue;
511
- }
512
- const type = asTrimmedString(item.type).toLowerCase();
513
- if (RESPONSES_TOOL_CALL_INPUT_TYPES.has(type)) {
514
- const callId = asTrimmedString(item.call_id || item.id);
515
- if (callId) seenToolCallIds.add(callId);
516
- sanitized.push(item);
517
- continue;
518
- }
519
- if (RESPONSES_TOOL_CALL_OUTPUT_TYPES.has(type)) {
520
- const callId = asTrimmedString(item.call_id || item.id);
521
- if (!callId || !seenToolCallIds.has(callId)) continue;
522
- sanitized.push(item);
523
- continue;
524
- }
525
- sanitized.push(item);
526
- }
527
- return sanitized;
528
- }
529
-
530
- function normalizeFreeformToolArguments(value) {
531
- if (typeof value === 'string') return stringifyJsonValue({ input: value }, '{"input":""}');
532
- if (value == null) return '{"input":""}';
533
- if (isRecord(value) && Object.prototype.hasOwnProperty.call(value, 'input')) {
534
- return stringifyJsonValue(value, '{"input":""}');
535
- }
536
- return stringifyJsonValue({ input: normalizeResponsesToolOutput(value) }, '{"input":""}');
537
- }
538
-
539
- function toOpenAiToolCall(item, fallbackIndex) {
540
- if (!isRecord(item)) return null;
541
- const callId = asTrimmedString(item.call_id || item.id) || `call_${crypto.randomBytes(8).toString('hex')}_${fallbackIndex}`;
542
- const name = asTrimmedString(item.name) || asTrimmedString(item.server_label);
543
- if (!name) return null;
544
- const type = asTrimmedString(item.type).toLowerCase();
545
- const rawArguments = item.arguments != null ? item.arguments : item.input;
546
- const args = type === 'custom_tool_call' && item.arguments == null
547
- ? normalizeFreeformToolArguments(rawArguments)
548
- : normalizeOpenAiToolArguments(rawArguments);
549
- return {
550
- id: callId,
551
- type: 'function',
552
- function: {
553
- name,
554
- arguments: args
555
- }
556
- };
557
- }
558
-
559
- function hasOpenAiMessageContent(content) {
560
- return typeof content === 'string'
561
- ? content.trim().length > 0
562
- : Array.isArray(content) && content.length > 0;
563
- }
564
-
565
- function normalizeResponsesInputToChatMessages(input) {
566
- // 参考 metapi 的 Responses → Chat 桥接:聚合连续 tool calls、丢弃孤儿 tool outputs,
567
- // 并保留 reasoning / richer content blocks / developer-role compatibility。
568
- const messages = [];
569
- const normalizedInput = stripOrphanedResponsesToolOutputs(input);
570
- let functionCallIndex = 0;
571
- let pendingToolCalls = [];
572
- const emittedToolCallIds = new Set();
573
-
574
- const flushPendingToolCalls = () => {
575
- if (pendingToolCalls.length <= 0) return;
576
- for (const toolCall of pendingToolCalls) {
577
- const callId = asTrimmedString(toolCall.id);
578
- if (callId) emittedToolCallIds.add(callId);
579
- }
580
- messages.push({
581
- role: 'assistant',
582
- content: null,
583
- tool_calls: pendingToolCalls
584
- });
585
- pendingToolCalls = [];
586
- };
587
-
588
- const pushToolOutputMessage = (callIdRaw, outputRaw) => {
589
- const toolCallId = asTrimmedString(callIdRaw);
590
- if (!toolCallId) return;
591
- messages.push({
592
- role: 'tool',
593
- tool_call_id: toolCallId,
594
- content: normalizeResponsesToolOutput(outputRaw)
595
- });
596
- };
597
-
598
- const processInputItem = (item) => {
599
- if (typeof item === 'string') {
600
- flushPendingToolCalls();
601
- const text = item.trim();
602
- if (text) messages.push({ role: 'user', content: text });
603
- return;
604
- }
605
- if (!isRecord(item)) return;
606
-
607
- const itemType = asTrimmedString(item.type).toLowerCase();
608
- if (itemType === 'function_call' || itemType === 'custom_tool_call') {
609
- const toolCall = toOpenAiToolCall(item, functionCallIndex);
610
- functionCallIndex += 1;
611
- if (toolCall) pendingToolCalls.push(toolCall);
612
- return;
613
- }
614
-
615
- if (itemType === 'function_call_output' || itemType === 'custom_tool_call_output') {
616
- flushPendingToolCalls();
617
- const toolCallId = asTrimmedString(item.call_id || item.id);
618
- if (!toolCallId || !emittedToolCallIds.has(toolCallId)) return;
619
- pushToolOutputMessage(toolCallId, item.output != null ? item.output : item.content);
620
- return;
621
- }
622
-
623
- if (itemType === 'reasoning') {
624
- // Any non-tool-call item is a sequence boundary: keep only consecutive
625
- // tool calls in the same assistant `tool_calls` message.
626
- flushPendingToolCalls();
627
- const reasoningContent = toOpenAiMessageContent(item.summary != null ? item.summary : (item.content != null ? item.content : item));
628
- const reasoningSignature = asTrimmedString(item.encrypted_content || item.reasoning_signature);
629
- if (!hasOpenAiMessageContent(reasoningContent) && !reasoningSignature) return;
630
- const message = { role: 'assistant', content: reasoningContent };
631
- if (reasoningSignature) message.reasoning_signature = reasoningSignature;
632
- messages.push(message);
633
- return;
634
- }
635
-
636
- flushPendingToolCalls();
637
- const role = asTrimmedString(item.role).toLowerCase() || 'user';
638
- const normalizedRole = role === 'developer' ? 'system' : role;
639
- const content = toOpenAiMessageContent(item.content != null ? item.content : (item.input != null ? item.input : item));
640
-
641
- if (normalizedRole === 'tool') {
642
- const toolCallId = asTrimmedString(item.tool_call_id || item.call_id || item.id);
643
- if (!toolCallId || !emittedToolCallIds.has(toolCallId)) return;
644
- pushToolOutputMessage(toolCallId, item.content);
645
- return;
646
- }
647
-
648
- if (!hasOpenAiMessageContent(content)) return;
649
- const message = { role: normalizedRole, content };
650
- const phase = asTrimmedString(item.phase);
651
- if (phase) message.phase = phase;
652
- messages.push(message);
653
- };
654
-
655
- if (typeof normalizedInput === 'string') {
656
- const text = normalizedInput.trim();
657
- if (text) messages.push({ role: 'user', content: text });
658
- } else if (Array.isArray(normalizedInput)) {
659
- for (const item of normalizedInput) processInputItem(item);
660
- } else if (isRecord(normalizedInput)) {
661
- processInputItem(normalizedInput);
662
- }
663
- flushPendingToolCalls();
664
- return messages;
665
- }
666
-
667
- function normalizeFunctionToolForChat(tool) {
668
- if (!isRecord(tool)) return null;
669
- const sourceFn = isRecord(tool.function) ? tool.function : tool;
670
- const name = asTrimmedString(sourceFn.name) || asTrimmedString(tool.name);
671
- if (!name) return null;
672
- const fn = { name };
673
- const description = asTrimmedString(sourceFn.description) || asTrimmedString(tool.description);
674
- if (description) fn.description = description;
675
- if (sourceFn.parameters !== undefined) {
676
- fn.parameters = cloneJsonValue(sourceFn.parameters);
677
- } else if (tool.parameters !== undefined) {
678
- fn.parameters = cloneJsonValue(tool.parameters);
679
- }
680
- if (typeof sourceFn.strict === 'boolean') {
681
- fn.strict = sourceFn.strict;
682
- } else if (typeof tool.strict === 'boolean') {
683
- fn.strict = tool.strict;
684
- }
685
- return { type: 'function', function: fn };
686
- }
687
-
688
- function buildLocalShellToolForChat(tool) {
689
- return {
690
- type: 'function',
691
- function: {
692
- name: asTrimmedString(tool && tool.name) || 'local_shell',
693
- description: asTrimmedString(tool && tool.description) || 'Run a local shell command and return its output.',
694
- parameters: {
695
- type: 'object',
696
- properties: {
697
- cmd: { type: 'string', description: 'Shell command to execute.' },
698
- yield_time_ms: { type: 'number', description: 'Milliseconds to wait before yielding partial output.' },
699
- max_output_tokens: { type: 'number', description: 'Maximum output tokens to return.' }
700
- },
701
- required: ['cmd'],
702
- additionalProperties: true
703
- }
704
- }
705
- };
706
- }
707
-
708
- function buildFreeformToolForChat(tool, fallbackName = 'custom_tool') {
709
- return {
710
- type: 'function',
711
- function: {
712
- name: asTrimmedString(tool && tool.name) || fallbackName,
713
- description: asTrimmedString(tool && tool.description) || 'Pass raw freeform input to the local tool.',
714
- parameters: {
715
- type: 'object',
716
- properties: {
717
- input: { type: 'string', description: 'Raw tool input.' }
718
- },
719
- required: ['input'],
720
- additionalProperties: false
721
- }
722
- }
723
- };
724
- }
725
-
726
- const MAX_RESPONSES_TOOL_NAMESPACE_DEPTH = 5;
727
-
728
- function rememberResponsesToolType(tool, target, depth = 0) {
729
- if (!isRecord(tool) || !target || depth > MAX_RESPONSES_TOOL_NAMESPACE_DEPTH) return;
730
- const type = asTrimmedString(tool.type).toLowerCase();
731
- if (type === 'namespace' && Array.isArray(tool.tools)) {
732
- for (const inner of tool.tools) rememberResponsesToolType(inner, target, depth + 1);
733
- return;
734
- }
735
- const sourceFn = isRecord(tool.function) ? tool.function : tool;
736
- const name = asTrimmedString(sourceFn.name) || asTrimmedString(tool.name);
737
- if (!name) return;
738
- if (type === 'local_shell') {
739
- target[name] = 'local_shell_call';
740
- return;
741
- }
742
- if (type === 'custom' || type === 'custom_tool' || (!type && name === 'apply_patch')) {
743
- target[name] = 'custom_tool_call';
744
- return;
745
- }
746
- if (type === 'function') {
747
- target[name] = 'function_call';
748
- }
749
- }
750
-
751
- function collectResponsesToolTypesByName(tools) {
752
- const result = {};
753
- if (!Array.isArray(tools)) return result;
754
- for (const tool of tools) rememberResponsesToolType(tool, result);
755
- return result;
756
- }
757
-
758
- function extractFreeformInputFromChatArguments(argumentsText) {
759
- if (typeof argumentsText !== 'string') return '';
760
- const parsed = parseJsonValueOrNull(argumentsText);
761
- if (isRecord(parsed) && Object.prototype.hasOwnProperty.call(parsed, 'input')) {
762
- return typeof parsed.input === 'string' ? parsed.input : normalizeResponsesToolOutput(parsed.input);
763
- }
764
- return argumentsText;
765
- }
766
-
767
- function extractLocalShellActionFromChatArguments(argumentsText) {
768
- const parsed = parseJsonValueOrNull(argumentsText);
769
- if (isRecord(parsed)) return cloneJsonValue(parsed);
770
- return { cmd: typeof argumentsText === 'string' ? argumentsText : '' };
771
- }
772
-
773
- function buildResponsesToolCallItemFromChatToolCall(toolCall, toolTypesByName = {}) {
774
- if (!isRecord(toolCall)) return null;
775
- const fn = isRecord(toolCall.function) ? toolCall.function : {};
776
- const name = asTrimmedString(fn.name);
777
- if (!name) return null;
778
- const callId = asTrimmedString(toolCall.id) || `call_${crypto.randomBytes(8).toString('hex')}`;
779
- const argumentsText = typeof fn.arguments === 'string' ? fn.arguments : '';
780
- const responseType = toolTypesByName && toolTypesByName[name] ? toolTypesByName[name] : 'function_call';
781
- if (responseType === 'custom_tool_call') {
782
- return {
783
- type: 'custom_tool_call',
784
- call_id: callId,
785
- name,
786
- input: extractFreeformInputFromChatArguments(argumentsText)
787
- };
788
- }
789
- if (responseType === 'local_shell_call') {
790
- return {
791
- type: 'local_shell_call',
792
- call_id: callId,
793
- name,
794
- action: extractLocalShellActionFromChatArguments(argumentsText)
795
- };
796
- }
797
- return {
798
- type: 'function_call',
799
- call_id: callId,
800
- name,
801
- arguments: argumentsText
802
- };
803
- }
804
-
805
- function normalizeSingleResponsesToolToChatTools(tool, depth = 0) {
806
- if (!isRecord(tool) || depth > MAX_RESPONSES_TOOL_NAMESPACE_DEPTH) return [];
807
- const type = asTrimmedString(tool.type).toLowerCase();
808
- if (type === 'namespace' && Array.isArray(tool.tools)) {
809
- return tool.tools.flatMap((inner) => normalizeSingleResponsesToolToChatTools(inner, depth + 1));
810
- }
811
- if (type === 'function') {
812
- const converted = normalizeFunctionToolForChat(tool);
813
- return converted ? [converted] : [];
814
- }
815
- if (type === 'local_shell') {
816
- return [buildLocalShellToolForChat(tool)];
817
- }
818
- const name = asTrimmedString(tool.name);
819
- if (type === 'custom' || type === 'custom_tool' || (!type && name === 'apply_patch')) {
820
- return [buildFreeformToolForChat(tool, name || 'custom_tool')];
821
- }
822
- // Hosted Responses tools such as web_search/image_generation/computer_use
823
- // do not have a safe Chat Completions representation. Passing them through
824
- // as-is makes OpenAI-compatible chat gateways reject the request, so drop
825
- // them instead of pretending the shapes are compatible.
826
- return [];
827
- }
828
-
829
- function normalizeResponsesToolsToChatTools(tools) {
830
- if (!Array.isArray(tools)) return tools;
831
- return tools.flatMap((tool) => normalizeSingleResponsesToolToChatTools(tool));
832
- }
833
-
834
- function normalizeResponsesToolChoiceToChatToolChoice(toolChoice) {
835
- if (toolChoice === undefined) return undefined;
836
- if (typeof toolChoice === 'string') return toolChoice;
837
- if (!isRecord(toolChoice)) return toolChoice;
838
-
839
- const type = asTrimmedString(toolChoice.type).toLowerCase();
840
- if (type === 'tool' || type === 'function' || type === 'custom' || type === 'custom_tool' || type === 'local_shell') {
841
- if (isRecord(toolChoice.function) && asTrimmedString(toolChoice.function.name)) return cloneJsonValue(toolChoice);
842
- const name = asTrimmedString(toolChoice.name) || asTrimmedString(toolChoice.server_label);
843
- if (!name) return 'required';
844
- return { type: 'function', function: { name } };
845
- }
846
- if (type === 'auto' || type === 'none' || type === 'required') return type;
847
- return 'auto';
848
- }
849
-
850
- function getChatToolChoiceName(toolChoice) {
851
- if (!isRecord(toolChoice)) return '';
852
- if (isRecord(toolChoice.function)) return asTrimmedString(toolChoice.function.name);
853
- return '';
854
- }
855
-
856
- function pruneInvalidChatToolChoice(chatBody) {
857
- if (!isRecord(chatBody) || !Array.isArray(chatBody.tools)) return;
858
- if (chatBody.tools.length === 0) {
859
- delete chatBody.tools;
860
- delete chatBody.tool_choice;
861
- return;
862
- }
863
- const chosenName = getChatToolChoiceName(chatBody.tool_choice);
864
- if (!chosenName) return;
865
- const toolNames = new Set(chatBody.tools
866
- .map((tool) => isRecord(tool) && isRecord(tool.function) ? asTrimmedString(tool.function.name) : '')
867
- .filter(Boolean));
868
- if (!toolNames.has(chosenName)) {
869
- delete chatBody.tool_choice;
870
- }
871
- }
872
-
873
- function buildChatCompletionsBodyFromResponsesPayload(payload) {
874
- const source = isRecord(payload) ? payload : {};
875
- const messages = normalizeResponsesInputToChatMessages(source.input);
876
- const instructions = asTrimmedString(source.instructions);
877
- if (instructions) {
878
- messages.unshift({ role: 'system', content: instructions });
879
- }
880
-
881
- const chatBody = {
882
- model: typeof source.model === 'string' ? source.model : '',
883
- messages,
884
- stream: false
885
- };
886
-
887
- const passthroughKeys = [
888
- 'frequency_penalty',
889
- 'presence_penalty',
890
- 'stop',
891
- 'temperature',
892
- 'top_p',
893
- 'tools',
894
- 'tool_choice',
895
- 'parallel_tool_calls',
896
- 'logprobs',
897
- 'top_logprobs',
898
- 'kbs',
899
- 'is_online',
900
- 'user',
901
- 'seed',
902
- 'n',
903
- 'modalities',
904
- 'audio',
905
- 'reasoning',
906
- 'reasoning_effort',
907
- 'service_tier'
908
- ];
909
- for (const key of passthroughKeys) {
910
- if (Object.prototype.hasOwnProperty.call(source, key)) {
911
- if (key === 'tools') {
912
- chatBody[key] = normalizeResponsesToolsToChatTools(source[key]);
913
- } else if (key === 'tool_choice') {
914
- chatBody[key] = normalizeResponsesToolChoiceToChatToolChoice(source[key]);
915
- } else {
916
- chatBody[key] = cloneJsonValue(source[key]);
917
- }
918
- }
919
- }
920
-
921
- if (Object.prototype.hasOwnProperty.call(source, 'response_format')) {
922
- chatBody.response_format = cloneJsonValue(source.response_format);
923
- } else if (isRecord(source.text) && source.text.format !== undefined) {
924
- chatBody.response_format = cloneJsonValue(source.text.format);
925
- }
926
- if (isRecord(source.text) && asTrimmedString(source.text.verbosity)) {
927
- chatBody.verbosity = asTrimmedString(source.text.verbosity);
928
- }
929
-
930
- pruneInvalidChatToolChoice(chatBody);
931
-
932
- if (Object.prototype.hasOwnProperty.call(source, 'max_tokens')) {
933
- chatBody.max_tokens = source.max_tokens;
934
- } else if (source.max_output_tokens != null) {
935
- chatBody.max_tokens = source.max_output_tokens;
936
- }
937
-
938
- return chatBody;
939
- }
940
-
941
- function ensureResponseMetadata(payload) {
942
- const base = payload && typeof payload === 'object' && !Array.isArray(payload) ? payload : {};
943
- const id = typeof base.id === 'string' && base.id.trim()
944
- ? base.id.trim()
945
- : `resp_${crypto.randomBytes(10).toString('hex')}`;
946
- const model = typeof base.model === 'string' ? base.model : '';
947
- return {
948
- object: 'response',
949
- id,
950
- model,
951
- ...base
952
- };
953
- }
954
-
955
- function writeSse(res, eventName, dataObj) {
956
- if (eventName) {
957
- res.write(`event: ${eventName}\n`);
958
- }
959
- if (dataObj === '[DONE]') {
960
- res.write('data: [DONE]\n\n');
961
- return;
962
- }
963
- res.write(`data: ${JSON.stringify(dataObj)}\n\n`);
964
- }
965
-
966
- function sendResponsesSse(res, responsePayload) {
967
- const response = ensureResponseMetadata(responsePayload);
968
- const responseId = response.id;
969
- const model = response.model;
970
- let sequence = 0;
971
- const nextSeq = () => {
972
- sequence += 1;
973
- return sequence;
974
- };
975
-
976
- writeSse(res, 'response.created', {
977
- type: 'response.created',
978
- response: {
979
- id: responseId,
980
- model,
981
- created_at: response.created_at
982
- }
983
- });
984
-
985
- const output = Array.isArray(response.output) ? response.output : [];
986
- for (let outputIndex = 0; outputIndex < output.length; outputIndex += 1) {
987
- const item = output[outputIndex];
988
- if (!item || typeof item !== 'object') continue;
989
- const itemType = typeof item.type === 'string' ? item.type : '';
990
- const itemId = typeof item.id === 'string' && item.id.trim()
991
- ? item.id.trim()
992
- : `item_${crypto.randomBytes(8).toString('hex')}`;
993
-
994
- writeSse(res, 'response.output_item.added', {
995
- type: 'response.output_item.added',
996
- output_index: outputIndex,
997
- item: { ...item, id: itemId }
998
- });
999
-
1000
- if (itemType === 'message') {
1001
- const content = Array.isArray(item.content) ? item.content : [];
1002
- for (let contentIndex = 0; contentIndex < content.length; contentIndex += 1) {
1003
- const block = content[contentIndex];
1004
- if (!block || typeof block !== 'object') continue;
1005
- if (block.type !== 'output_text') continue;
1006
- const text = typeof block.text === 'string' ? block.text : '';
1007
- if (text) {
1008
- writeSse(res, 'response.output_text.delta', {
1009
- type: 'response.output_text.delta',
1010
- item_id: itemId,
1011
- output_index: outputIndex,
1012
- content_index: contentIndex,
1013
- delta: text,
1014
- sequence_number: nextSeq()
1015
- });
1016
- }
1017
- writeSse(res, 'response.output_text.done', {
1018
- type: 'response.output_text.done',
1019
- item_id: itemId,
1020
- output_index: outputIndex,
1021
- content_index: contentIndex,
1022
- text,
1023
- sequence_number: nextSeq()
1024
- });
1025
- }
1026
- }
1027
-
1028
- writeSse(res, 'response.output_item.done', {
1029
- type: 'response.output_item.done',
1030
- output_index: outputIndex,
1031
- item: { ...item, id: itemId },
1032
- sequence_number: nextSeq()
1033
- });
1034
- }
1035
-
1036
- writeSse(res, 'response.completed', { type: 'response.completed', response });
1037
- writeSse(res, 'done', '[DONE]');
1038
- }
1039
-
1040
- function appendChatStreamToolCall(target, toolCall) {
1041
- if (!toolCall || typeof toolCall !== 'object') return;
1042
- const index = Number.isFinite(toolCall.index) ? toolCall.index : target.length;
1043
- if (!target[index]) {
1044
- target[index] = {
1045
- id: '',
1046
- type: 'function',
1047
- function: { name: '', arguments: '' }
1048
- };
1049
- }
1050
- const current = target[index];
1051
- if (typeof toolCall.id === 'string' && toolCall.id) current.id = toolCall.id;
1052
- if (typeof toolCall.type === 'string' && toolCall.type) current.type = toolCall.type;
1053
- const fn = toolCall.function && typeof toolCall.function === 'object' ? toolCall.function : null;
1054
- if (fn) {
1055
- if (typeof fn.name === 'string' && fn.name) current.function.name = fn.name;
1056
- if (typeof fn.arguments === 'string') current.function.arguments += fn.arguments;
1057
- }
1058
- }
1059
-
1060
- function writeChatCompletionChunkAsResponsesSse(state, chunk) {
1061
- if (!chunk || typeof chunk !== 'object') return;
1062
- if (typeof chunk.model === 'string' && chunk.model) {
1063
- state.model = chunk.model;
1064
- }
1065
- const choices = Array.isArray(chunk.choices) ? chunk.choices : [];
1066
- for (const choice of choices) {
1067
- const delta = choice && choice.delta && typeof choice.delta === 'object' ? choice.delta : null;
1068
- if (!delta) continue;
1069
-
1070
- if (typeof delta.content === 'string' && delta.content) {
1071
- if (!state.messageItem) {
1072
- state.messageItem = {
1073
- id: `msg_${crypto.randomBytes(8).toString('hex')}`,
1074
- type: 'message',
1075
- role: 'assistant',
1076
- content: [{ type: 'output_text', text: '' }]
1077
- };
1078
- state.output.push(state.messageItem);
1079
- state.outputStarted = true;
1080
- beginChatStreamResponsesSse(state);
1081
- writeSse(state.res, 'response.output_item.added', {
1082
- type: 'response.output_item.added',
1083
- output_index: state.output.length - 1,
1084
- item: state.messageItem
1085
- });
1086
- }
1087
- state.messageText += delta.content;
1088
- state.messageItem.content[0].text = state.messageText;
1089
- writeSse(state.res, 'response.output_text.delta', {
1090
- type: 'response.output_text.delta',
1091
- item_id: state.messageItem.id,
1092
- output_index: state.output.length - 1,
1093
- content_index: 0,
1094
- delta: delta.content,
1095
- sequence_number: state.nextSeq()
1096
- });
1097
- }
1098
-
1099
- if (Array.isArray(delta.tool_calls)) {
1100
- for (const toolCall of delta.tool_calls) {
1101
- appendChatStreamToolCall(state.toolCalls, toolCall);
1102
- }
1103
- }
1104
- }
1105
- }
1106
-
1107
- function stopChatStreamHeartbeat(state) {
1108
- if (!state || !state.heartbeatTimer) return;
1109
- clearInterval(state.heartbeatTimer);
1110
- state.heartbeatTimer = null;
1111
- }
1112
-
1113
- function startChatStreamHeartbeat(state) {
1114
- if (!state || state.heartbeatTimer) return;
1115
- const timer = setInterval(() => {
1116
- if (state.finished) {
1117
- stopChatStreamHeartbeat(state);
1118
- return;
1119
- }
1120
- const target = state.res;
1121
- if (!target || target.writableEnded || target.destroyed) {
1122
- stopChatStreamHeartbeat(state);
1123
- return;
1124
- }
1125
- try { target.write(': keepalive\n\n'); } catch (_) {}
1126
- }, 15000);
1127
- if (typeof timer.unref === 'function') timer.unref();
1128
- state.heartbeatTimer = timer;
1129
- }
1130
-
1131
- function finishChatStreamResponsesSse(state) {
1132
- if (state.finished) return;
1133
- beginChatStreamResponsesSse(state);
1134
- state.finished = true;
1135
- stopChatStreamHeartbeat(state);
1136
-
1137
- if (state.messageItem) {
1138
- const outputIndex = state.output.indexOf(state.messageItem);
1139
- writeSse(state.res, 'response.output_text.done', {
1140
- type: 'response.output_text.done',
1141
- item_id: state.messageItem.id,
1142
- output_index: outputIndex,
1143
- content_index: 0,
1144
- text: state.messageText,
1145
- sequence_number: state.nextSeq()
1146
- });
1147
- writeSse(state.res, 'response.output_item.done', {
1148
- type: 'response.output_item.done',
1149
- output_index: outputIndex,
1150
- item: state.messageItem,
1151
- sequence_number: state.nextSeq()
1152
- });
1153
- }
1154
-
1155
- for (const toolCall of state.toolCalls) {
1156
- if (!toolCall) continue;
1157
- const item = buildResponsesToolCallItemFromChatToolCall(toolCall, state.toolTypesByName || {});
1158
- if (!item) continue;
1159
- const outputIndex = state.output.length;
1160
- state.output.push(item);
1161
- state.outputStarted = true;
1162
- writeSse(state.res, 'response.output_item.added', {
1163
- type: 'response.output_item.added',
1164
- output_index: outputIndex,
1165
- item
1166
- });
1167
- writeSse(state.res, 'response.output_item.done', {
1168
- type: 'response.output_item.done',
1169
- output_index: outputIndex,
1170
- item,
1171
- sequence_number: state.nextSeq()
1172
- });
1173
- }
1174
-
1175
- const response = ensureResponseMetadata({
1176
- id: state.responseId,
1177
- model: state.model,
1178
- created_at: state.createdAt,
1179
- status: 'completed',
1180
- output: state.output
1181
- });
1182
- writeSse(state.res, 'response.completed', { type: 'response.completed', response });
1183
- writeSse(state.res, 'done', '[DONE]');
1184
- state.res.end();
1185
- }
1186
-
1187
- function failResponsesSseRaw(res, message) {
1188
- if (!res || res.writableEnded || res.destroyed) return;
1189
- try {
1190
- writeSse(res, 'response.failed', { type: 'response.failed', error: message || 'upstream stream failed' });
1191
- writeSse(res, 'done', '[DONE]');
1192
- res.end();
1193
- } catch (_) {}
1194
- }
1195
-
1196
- function beginChatStreamResponsesSse(state) {
1197
- if (!state || state.started) return;
1198
- state.started = true;
1199
- const res = state.res;
1200
- if (!res.headersSent) {
1201
- res.writeHead(200, {
1202
- 'Content-Type': 'text/event-stream; charset=utf-8',
1203
- 'Cache-Control': 'no-cache',
1204
- 'Connection': 'keep-alive',
1205
- 'X-Accel-Buffering': 'no'
1206
- });
1207
- }
1208
- startChatStreamHeartbeat(state);
1209
- if (typeof res.on === 'function' && !state.closeListenerAttached) {
1210
- state.closeListenerAttached = true;
1211
- res.on('close', () => {
1212
- stopChatStreamHeartbeat(state);
1213
- if (!state.finished && state.upstreamReq) {
1214
- try { state.upstreamReq.destroy(new Error('client aborted')); } catch (_) {}
1215
- }
1216
- });
1217
- }
1218
- writeSse(res, 'response.created', {
1219
- type: 'response.created',
1220
- response: {
1221
- id: state.responseId,
1222
- model: state.model,
1223
- created_at: state.createdAt
1224
- }
1225
- });
1226
- }
1227
-
1228
- function failChatStreamResponsesSse(state, message) {
1229
- if (!state || state.finished) return;
1230
- beginChatStreamResponsesSse(state);
1231
- state.finished = true;
1232
- stopChatStreamHeartbeat(state);
1233
- failResponsesSseRaw(state.res, message);
1234
- }
1235
-
1236
- function createChatStreamResponsesSseState(res, model, options = {}) {
1237
- let sequence = 0;
1238
- return {
1239
- res,
1240
- upstreamReq: null,
1241
- responseId: `resp_${crypto.randomBytes(10).toString('hex')}`,
1242
- model: typeof model === 'string' ? model : '',
1243
- createdAt: Math.floor(Date.now() / 1000),
1244
- output: [],
1245
- messageItem: null,
1246
- messageText: '',
1247
- toolCalls: [],
1248
- toolTypesByName: options.toolTypesByName || {},
1249
- finished: false,
1250
- started: false,
1251
- outputStarted: false,
1252
- closeListenerAttached: false,
1253
- nextSeq: () => {
1254
- sequence += 1;
1255
- return sequence;
1256
- }
1257
- };
1258
- }
1259
-
1260
- function streamChatCompletionsAsResponsesSse(targetUrl, options = {}) {
1261
- const parsed = new URL(targetUrl);
1262
- const transport = parsed.protocol === 'https:' ? https : http;
1263
- const bodyText = options.body ? JSON.stringify(options.body) : '';
1264
- const headers = {
1265
- 'Accept': 'text/event-stream',
1266
- ...(options.body ? { 'Content-Type': 'application/json' } : {}),
1267
- ...(options.headers || {})
1268
- };
1269
- if (options.body) {
1270
- headers['Content-Length'] = Buffer.byteLength(bodyText, 'utf-8');
1271
- }
1272
- const timeoutMs = Number.isFinite(options.timeoutMs)
1273
- ? Math.max(1000, Number(options.timeoutMs))
1274
- : 30000;
1275
- const res = options.res;
1276
- const model = typeof options.model === 'string' ? options.model : '';
1277
- const sharedState = options.streamState || createChatStreamResponsesSseState(res, model, {
1278
- toolTypesByName: options.toolTypesByName || {}
1279
- });
1280
-
1281
- return new Promise((resolve) => {
1282
- let settled = false;
1283
- let streamAccepted = false;
1284
- const finish = (value) => {
1285
- if (settled) return;
1286
- settled = true;
1287
- resolve(value);
1288
- };
1289
- const req = transport.request({
1290
- protocol: parsed.protocol,
1291
- hostname: parsed.hostname,
1292
- port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
1293
- method: options.method || 'POST',
1294
- path: `${parsed.pathname}${parsed.search}`,
1295
- headers,
1296
- agent: parsed.protocol === 'https:' ? HTTPS_KEEP_ALIVE_AGENT : HTTP_KEEP_ALIVE_AGENT
1297
- }, (upstreamRes) => {
1298
- const status = upstreamRes.statusCode || 0;
1299
- const chunks = [];
1300
- const contentType = String(upstreamRes.headers && upstreamRes.headers['content-type'] || '');
1301
- streamAccepted = status >= 200 && status < 300 && /text\/event-stream/i.test(contentType);
1302
- if (streamAccepted) {
1303
- req.setTimeout(0);
1304
- }
1305
- let streamState = null;
1306
-
1307
- const handleAbort = (reason) => {
1308
- if (settled) return;
1309
- if (streamState) {
1310
- if (streamState.outputStarted) {
1311
- failChatStreamResponsesSse(streamState, reason);
1312
- finish({ ok: true });
1313
- return;
1314
- }
1315
- finish({ ok: false, retryTransient: true, error: reason || 'upstream stream failed' });
1316
- return;
1317
- }
1318
- if (res.headersSent) {
1319
- failResponsesSseRaw(res, reason);
1320
- finish({ ok: true });
1321
- return;
1322
- }
1323
- const bodyText = chunks.length ? Buffer.concat(chunks).toString('utf-8') : '';
1324
- const transient = isTransientNetworkError(reason) || /aborted|stream aborted/i.test(String(reason || ''));
1325
- finish({
1326
- ok: false,
1327
- ...(transient ? {} : { status }),
1328
- ...(transient ? { retryTransient: true } : {}),
1329
- error: reason,
1330
- bodyText
1331
- });
1332
- };
1333
- upstreamRes.on('error', (err) => handleAbort(err && err.message ? err.message : 'upstream stream failed'));
1334
- upstreamRes.on('aborted', () => handleAbort('upstream stream aborted'));
1335
-
1336
- if (status === 404 || status === 405) {
1337
- upstreamRes.on('data', (chunk) => chunk && chunks.push(chunk));
1338
- upstreamRes.on('end', () => finish({ retry: true, status, bodyText: chunks.length ? Buffer.concat(chunks).toString('utf-8') : '' }));
1339
- return;
1340
- }
1341
-
1342
- if (status >= 400) {
1343
- upstreamRes.on('data', (chunk) => chunk && chunks.push(chunk));
1344
- upstreamRes.on('end', () => finish({ ok: false, status, bodyText: chunks.length ? Buffer.concat(chunks).toString('utf-8') : '' }));
1345
- return;
1346
- }
1347
-
1348
- if (!/text\/event-stream/i.test(contentType)) {
1349
- upstreamRes.on('data', (chunk) => chunk && chunks.push(chunk));
1350
- upstreamRes.on('end', () => {
1351
- const text = chunks.length ? Buffer.concat(chunks).toString('utf-8') : '';
1352
- const parsedJson = parseJsonOrError(text);
1353
- res.writeHead(200, {
1354
- 'Content-Type': 'text/event-stream; charset=utf-8',
1355
- 'Cache-Control': 'no-cache',
1356
- 'Connection': 'keep-alive',
1357
- 'X-Accel-Buffering': 'no'
1358
- });
1359
- if (parsedJson.error) {
1360
- writeSse(res, 'response.failed', { type: 'response.failed', error: `invalid upstream response: ${parsedJson.error}` });
1361
- writeSse(res, 'done', '[DONE]');
1362
- res.end();
1363
- finish({ ok: true });
1364
- return;
1365
- }
1366
- sendResponsesSse(res, buildResponsesPayloadFromChatCompletion(parsedJson.value, model, {
1367
- toolTypesByName: options.toolTypesByName || {}
1368
- }));
1369
- res.end();
1370
- finish({ ok: true });
1371
- });
1372
- return;
1373
- }
1374
-
1375
- const state = sharedState;
1376
- state.upstreamReq = req;
1377
- if (!state.model && model) state.model = model;
1378
- streamState = state;
1379
- beginChatStreamResponsesSse(state);
1380
-
1381
- let buffer = '';
1382
- const handleEventBlock = (block) => {
1383
- const dataLines = String(block || '')
1384
- .split(/\r?\n/)
1385
- .filter((line) => line.startsWith('data:'))
1386
- .map((line) => line.slice(5).trimStart());
1387
- if (dataLines.length === 0) return;
1388
- const data = dataLines.join('\n').trim();
1389
- if (!data) return;
1390
- if (data === '[DONE]') {
1391
- finishChatStreamResponsesSse(state);
1392
- finish({ ok: true });
1393
- return;
1394
- }
1395
- const parsedChunk = parseJsonOrError(data);
1396
- if (!parsedChunk.error) {
1397
- beginChatStreamResponsesSse(state);
1398
- writeChatCompletionChunkAsResponsesSse(state, parsedChunk.value);
1399
- }
1400
- };
1401
-
1402
- upstreamRes.on('data', (chunk) => {
1403
- buffer += chunk.toString('utf-8');
1404
- let boundary = buffer.search(/\r?\n\r?\n/);
1405
- while (boundary >= 0) {
1406
- const block = buffer.slice(0, boundary);
1407
- const match = buffer.slice(boundary).match(/^\r?\n\r?\n/);
1408
- buffer = buffer.slice(boundary + (match ? match[0].length : 2));
1409
- handleEventBlock(block);
1410
- boundary = buffer.search(/\r?\n\r?\n/);
1411
- }
1412
- });
1413
- upstreamRes.on('end', () => {
1414
- if (buffer.trim()) handleEventBlock(buffer);
1415
- finishChatStreamResponsesSse(state);
1416
- finish({ ok: true });
1417
- });
1418
- });
1419
- req.setTimeout(timeoutMs, () => {
1420
- if (streamAccepted) return;
1421
- try { req.destroy(new Error('timeout')); } catch (_) {}
1422
- finish({ ok: false, error: 'timeout' });
1423
- });
1424
- req.on('error', (err) => finish({ ok: false, error: err && err.message ? err.message : 'request failed' }));
1425
- if (bodyText) req.write(bodyText);
1426
- req.end();
1427
- });
1428
- }
1429
-
1430
- async function streamChatCompletionsAsResponsesSseWithFallbackUrls(baseUrl, pathSuffix, options = {}) {
1431
- const urls = buildUpstreamUrlCandidates(baseUrl, pathSuffix);
1432
- if (urls.length === 0) {
1433
- return { ok: false, error: 'failed to build upstream URL' };
1434
- }
1435
- let lastResult = null;
1436
- const streamState = options.streamState || createChatStreamResponsesSseState(options.res, options.model, {
1437
- toolTypesByName: options.toolTypesByName || {}
1438
- });
1439
- for (const url of urls) {
1440
- const result = await retryTransientRequest(() => streamChatCompletionsAsResponsesSse(url, { ...options, streamState }));
1441
- lastResult = result;
1442
- if (result && result.retry) continue;
1443
- return result;
1444
- }
1445
- return lastResult || { ok: false, error: 'failed to build upstream URL' };
1446
- }
1447
-
1448
- function canListenPort(host, port) {
1449
- return new Promise((resolve) => {
1450
- const tester = net.createServer();
1451
- tester.unref();
1452
- tester.once('error', () => resolve(false));
1453
- tester.once('listening', () => {
1454
- tester.close(() => resolve(true));
1455
- });
1456
- tester.listen(port, host);
1457
- });
1458
- }
1459
-
1460
- async function findAvailablePort(host, startPort, maxAttempts = 20) {
1461
- const start = parseInt(String(startPort), 10);
1462
- if (!Number.isFinite(start) || start <= 0) {
1463
- return 0;
1464
- }
1465
- const attempts = Number.isFinite(maxAttempts) && maxAttempts > 0 ? maxAttempts : 20;
1466
- for (let offset = 0; offset < attempts; offset += 1) {
1467
- const candidate = start + offset;
1468
- if (candidate > 65535) {
1469
- break;
1470
- }
1471
- // eslint-disable-next-line no-await-in-loop
1472
- const ok = await canListenPort(host, candidate);
1473
- if (ok) {
1474
- return candidate;
1475
- }
1476
- }
1477
- return 0;
1478
- }
1479
-
1480
- function resolveBuiltinProxyProviderName(rawProviderName, providers = {}, preferredProvider = '') {
1481
- const providerMap = providers && isPlainObject(providers) ? providers : {};
1482
- const providerNames = Object.keys(providerMap)
1483
- .filter((name) => name && !isBuiltinManagedProvider(name));
1484
- const requested = typeof rawProviderName === 'string' ? rawProviderName.trim() : '';
1485
- if (requested && !isBuiltinManagedProvider(requested) && providerMap[requested]) {
1486
- return requested;
1487
- }
1488
- const preferred = typeof preferredProvider === 'string' ? preferredProvider.trim() : '';
1489
- if (preferred && !isBuiltinManagedProvider(preferred) && providerMap[preferred]) {
1490
- return preferred;
1491
- }
1492
- return providerNames[0] || '';
1493
- }
1494
-
1495
- function normalizeBuiltinProxySettings(raw) {
1496
- const merged = {
1497
- ...DEFAULT_BUILTIN_PROXY_SETTINGS,
1498
- ...(isPlainObject(raw) ? raw : {})
1499
- };
1500
- const host = typeof merged.host === 'string' ? merged.host.trim() : '';
1501
- const port = parseInt(String(merged.port), 10);
1502
- const provider = typeof merged.provider === 'string' ? merged.provider.trim() : '';
1503
- const authSourceRaw = typeof merged.authSource === 'string' ? merged.authSource.trim().toLowerCase() : '';
1504
- const timeoutMs = parseInt(String(merged.timeoutMs), 10);
1505
- const authSource = authSourceRaw === 'profile' || authSourceRaw === 'none' ? authSourceRaw : 'provider';
1506
-
1507
- return {
1508
- enabled: merged.enabled !== false,
1509
- host: host || DEFAULT_BUILTIN_PROXY_SETTINGS.host,
1510
- port: Number.isFinite(port) && port > 0 && port <= 65535 ? port : DEFAULT_BUILTIN_PROXY_SETTINGS.port,
1511
- provider,
1512
- authSource,
1513
- timeoutMs: Number.isFinite(timeoutMs) && timeoutMs >= 1000
1514
- ? timeoutMs
1515
- : DEFAULT_BUILTIN_PROXY_SETTINGS.timeoutMs
1516
- };
1517
- }
1518
-
1519
- function readBuiltinProxySettings() {
1520
- const parsed = readJsonFile(BUILTIN_PROXY_SETTINGS_FILE, null);
1521
- return normalizeBuiltinProxySettings(parsed);
1522
- }
1523
-
1524
- function saveBuiltinProxySettings(payload = {}, options = {}) {
1525
- const current = readBuiltinProxySettings();
1526
- const merged = normalizeBuiltinProxySettings({
1527
- ...current,
1528
- ...(isPlainObject(payload) ? payload : {})
1529
- });
1530
-
1531
- if (!merged.host) {
1532
- return { error: '代理 host 不能为空' };
1533
- }
1534
- if (!Number.isFinite(merged.port) || merged.port <= 0 || merged.port > 65535) {
1535
- return { error: '代理端口无效(1-65535)' };
1536
- }
1537
-
1538
- const { config } = readConfigOrVirtualDefault();
1539
- const providers = config && isPlainObject(config.model_providers) ? config.model_providers : {};
1540
- const preferredProvider = typeof config.model_provider === 'string' ? config.model_provider.trim() : '';
1541
- const finalProvider = resolveBuiltinProxyProviderName(merged.provider, providers, preferredProvider);
1542
-
1543
- const normalized = {
1544
- ...merged,
1545
- provider: finalProvider
1546
- };
1547
-
1548
- if (!options.skipWrite) {
1549
- writeJsonAtomic(BUILTIN_PROXY_SETTINGS_FILE, normalized);
1550
- }
1551
-
1552
- return {
1553
- success: true,
1554
- settings: normalized
1555
- };
1556
- }
1557
-
1558
- function buildProxyListenUrl(settings) {
1559
- const host = formatHostForUrl(settings.host || DEFAULT_BUILTIN_PROXY_SETTINGS.host);
1560
- return `http://${host}:${settings.port}`;
1561
- }
1562
-
1563
- function buildBuiltinProxyProviderBaseUrl(settings) {
1564
- return `${buildProxyListenUrl(settings).replace(/\/+$/, '')}/v1`;
1565
- }
1566
-
1567
- function removePersistedBuiltinProxyProviderFromConfig() {
1568
- if (!fs.existsSync(CONFIG_FILE)) {
1569
- return { success: true, removed: false };
1570
- }
1571
-
1572
- let config;
1573
- try {
1574
- config = readConfig();
1575
- } catch (e) {
1576
- return { error: e.message || '读取 config.toml 失败' };
1577
- }
1578
-
1579
- if (!config.model_providers || !config.model_providers[BUILTIN_PROXY_PROVIDER_NAME]) {
1580
- return { success: true, removed: false };
1581
- }
1582
-
1583
- const content = fs.readFileSync(CONFIG_FILE, 'utf-8');
1584
- const lineEnding = content.includes('\r\n') ? '\r\n' : '\n';
1585
- const hasBom = content.charCodeAt(0) === 0xFEFF;
1586
- const providerConfig = config.model_providers[BUILTIN_PROXY_PROVIDER_NAME];
1587
- const providerSegments = providerConfig && Array.isArray(providerConfig.__codexmate_legacy_segments)
1588
- ? providerConfig.__codexmate_legacy_segments
1589
- : null;
1590
- const providerSegmentVariants = (() => {
1591
- const variants = [];
1592
- const seen = new Set();
1593
- const pushVariant = (segments) => {
1594
- const normalized = normalizeLegacySegments(segments);
1595
- const key = buildLegacySegmentsKey(normalized);
1596
- if (!key || seen.has(key)) return;
1597
- seen.add(key);
1598
- variants.push(normalized);
1599
- };
1600
- if (providerConfig && Array.isArray(providerConfig.__codexmate_legacy_segments)) {
1601
- pushVariant(providerConfig.__codexmate_legacy_segments);
1602
- }
1603
- if (providerConfig && Array.isArray(providerConfig.__codexmate_legacy_segment_variants)) {
1604
- for (const segments of providerConfig.__codexmate_legacy_segment_variants) {
1605
- pushVariant(segments);
1606
- }
1607
- }
1608
- if (providerSegments) {
1609
- pushVariant(providerSegments);
1610
- }
1611
- if (variants.length === 0) {
1612
- pushVariant(String(BUILTIN_PROXY_PROVIDER_NAME || '').split('.').filter((item) => item));
1613
- }
1614
- return variants;
1615
- })();
1616
-
1617
- let updatedContent = null;
1618
- const combinedRanges = [];
1619
- for (const segments of providerSegmentVariants) {
1620
- combinedRanges.push(...findProviderSectionRanges(content, BUILTIN_PROXY_PROVIDER_NAME, segments));
1621
- combinedRanges.push(...findProviderDescendantSectionRanges(content, segments));
1622
- }
1623
- if (combinedRanges.length === 0) {
1624
- combinedRanges.push(...findProviderSectionRanges(content, BUILTIN_PROXY_PROVIDER_NAME, providerSegments));
1625
- }
1626
-
1627
- if (combinedRanges.length > 0) {
1628
- const sorted = combinedRanges.sort((a, b) => b.start - a.start || b.end - a.end);
1629
- const seen = new Set();
1630
- let removedContent = content;
1631
- for (const range of sorted) {
1632
- const rangeKey = `${range.start}:${range.end}`;
1633
- if (seen.has(rangeKey)) continue;
1634
- seen.add(rangeKey);
1635
- removedContent = removedContent.slice(0, range.start) + removedContent.slice(range.end);
1636
- }
1637
- updatedContent = removedContent.replace(/\n{3,}/g, lineEnding + lineEnding);
1638
- }
1639
-
1640
- if (!updatedContent) {
1641
- const rebuilt = JSON.parse(JSON.stringify(config));
1642
- delete rebuilt.model_providers[BUILTIN_PROXY_PROVIDER_NAME];
1643
- const hasMarker = content.includes(CODEXMATE_MANAGED_MARKER);
1644
- let rebuiltToml = toml.stringify(rebuilt).trimEnd();
1645
- rebuiltToml = rebuiltToml.replace(/\n/g, lineEnding);
1646
- if (hasMarker && !rebuiltToml.includes(CODEXMATE_MANAGED_MARKER)) {
1647
- rebuiltToml = `${CODEXMATE_MANAGED_MARKER}${lineEnding}${rebuiltToml}`;
1648
- }
1649
- updatedContent = rebuiltToml + lineEnding;
1650
- if (hasBom && updatedContent.charCodeAt(0) !== 0xFEFF) {
1651
- updatedContent = '\uFEFF' + updatedContent;
1652
- }
1653
- }
1654
-
1655
- try {
1656
- writeConfig(updatedContent.trimEnd() + lineEnding);
1657
- } catch (e) {
1658
- return { error: e.message || '写入 config.toml 失败' };
1659
- }
1660
-
1661
- return { success: true, removed: true };
1662
- }
1663
-
1664
- function hasCodexConfigReadyForProxy() {
1665
- const result = readConfigOrVirtualDefault();
1666
- if (!result || result.isVirtual) {
1667
- return false;
1668
- }
1669
- const config = result.config || {};
1670
- if (!isPlainObject(config.model_providers)) {
1671
- return false;
1672
- }
1673
- const providerNames = Object.keys(config.model_providers)
1674
- .filter((name) => name && !isBuiltinManagedProvider(name));
1675
- return providerNames.length > 0;
1676
- }
1677
-
1678
- function resolveBuiltinProxyUpstream(settings) {
1679
- const { config } = readConfigOrVirtualDefault();
1680
- const providers = config && isPlainObject(config.model_providers) ? config.model_providers : {};
1681
- const currentProvider = typeof config.model_provider === 'string' ? config.model_provider.trim() : '';
1682
- const providerName = resolveBuiltinProxyProviderName(settings.provider, providers, currentProvider);
1683
- if (!providerName) {
1684
- return { error: '未找到可用的上游 provider,请先添加 provider' };
1685
- }
1686
- if (providerName === BUILTIN_PROXY_PROVIDER_NAME) {
1687
- return { error: `上游 provider 不能是 ${BUILTIN_PROXY_PROVIDER_NAME}` };
1688
- }
1689
- const provider = providers[providerName];
1690
- if (!provider || !isPlainObject(provider)) {
1691
- return { error: `上游 provider 不存在: ${providerName}` };
1692
- }
1693
-
1694
- const baseUrl = typeof provider.base_url === 'string' ? provider.base_url.trim() : '';
1695
- if (!baseUrl || !isValidHttpUrl(baseUrl)) {
1696
- return { error: `上游 provider base_url 无效: ${providerName}` };
1697
- }
1698
-
1699
- let token = '';
1700
- if (settings.authSource === 'profile') {
1701
- token = resolveAuthTokenFromCurrentProfile();
1702
- } else if (settings.authSource === 'provider') {
1703
- token = typeof provider.preferred_auth_method === 'string' ? provider.preferred_auth_method.trim() : '';
1704
- if (!token) {
1705
- token = resolveAuthTokenFromCurrentProfile();
1706
- }
1707
- }
1708
-
1709
- let authHeader = '';
1710
- if (token) {
1711
- authHeader = /^bearer\s+/i.test(token) ? token : `Bearer ${token}`;
1712
- }
1713
-
1714
- return {
1715
- providerName,
1716
- baseUrl: normalizeBaseUrl(baseUrl),
1717
- authHeader
1718
- };
1719
- }
1720
-
1721
- function createBuiltinProxyServer(settings, upstream) {
1722
- const connections = new Set();
1723
- const timeoutMs = settings.timeoutMs;
1724
- const server = http.createServer((req, res) => {
1725
- let parsedIncoming;
1726
- try {
1727
- parsedIncoming = new URL(req.url || '/', 'http://localhost');
1728
- } catch (e) {
1729
- res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
1730
- res.end(JSON.stringify({ error: 'invalid request path' }));
1731
- return;
1732
- }
1733
-
1734
- const remoteAddr = req && req.socket ? req.socket.remoteAddress : '';
1735
- const isLoopback = !remoteAddr
1736
- || remoteAddr === '127.0.0.1'
1737
- || remoteAddr === '::1'
1738
- || remoteAddr === '::ffff:127.0.0.1';
1739
- if (!isLoopback) {
1740
- const expected = typeof process.env.CODEXMATE_HTTP_TOKEN === 'string'
1741
- ? process.env.CODEXMATE_HTTP_TOKEN.trim()
1742
- : '';
1743
- if (!expected) {
1744
- const body = JSON.stringify({ error: 'Remote access is disabled (set CODEXMATE_HTTP_TOKEN)' });
1745
- res.writeHead(403, {
1746
- 'Content-Type': 'application/json; charset=utf-8',
1747
- 'Content-Length': Buffer.byteLength(body, 'utf-8')
1748
- });
1749
- res.end(body, 'utf-8');
1750
- return;
1751
- }
1752
- const headers = req && req.headers && typeof req.headers === 'object' ? req.headers : {};
1753
- const rawAuth = typeof headers.authorization === 'string' ? headers.authorization.trim() : '';
1754
- const match = rawAuth ? rawAuth.match(/^bearer\s+(.+)$/i) : null;
1755
- const actual = match && match[1]
1756
- ? match[1].trim()
1757
- : (rawAuth ? rawAuth : (typeof headers['x-codexmate-token'] === 'string' ? String(headers['x-codexmate-token']).trim() : ''));
1758
- if (!actual || actual !== expected) {
1759
- const body = JSON.stringify({ error: 'Unauthorized' });
1760
- res.writeHead(401, {
1761
- 'Content-Type': 'application/json; charset=utf-8',
1762
- 'Content-Length': Buffer.byteLength(body, 'utf-8')
1763
- });
1764
- res.end(body, 'utf-8');
1765
- return;
1766
- }
1767
- }
1768
-
1769
- const incomingPath = parsedIncoming.pathname || '/';
1770
- if (incomingPath === '/health' || incomingPath === '/status') {
1771
- const body = JSON.stringify({
1772
- ok: true,
1773
- upstreamProvider: upstream.providerName,
1774
- upstreamBaseUrl: upstream.baseUrl
1775
- });
1776
- res.writeHead(200, {
1777
- 'Content-Type': 'application/json; charset=utf-8',
1778
- 'Content-Length': Buffer.byteLength(body, 'utf-8')
1779
- });
1780
- res.end(body, 'utf-8');
1781
- return;
1782
- }
1783
-
1784
- if (!(incomingPath === '/v1' || incomingPath.startsWith('/v1/'))) {
1785
- const body = JSON.stringify({ error: 'proxy only supports /v1/* paths' });
1786
- res.writeHead(404, {
1787
- 'Content-Type': 'application/json; charset=utf-8',
1788
- 'Content-Length': Buffer.byteLength(body, 'utf-8')
1789
- });
1790
- res.end(body, 'utf-8');
1791
- return;
1792
- }
1793
-
1794
- // Responses shim:
1795
- // - Codex CLI 默认走 /v1/responses(含 SSE)
1796
- // - SSE/streaming 任务优先走 chat/completions fallback,避免卡在会接收但不产出 Responses 的兼容网关
1797
- // - 非流式请求仍优先尝试 /v1/responses(stream=false),失败再转换到 chat/completions 并回包为 responses。
1798
- if ((incomingPath === '/v1/responses' || incomingPath === '/v1/responses/') && (req.method || 'GET').toUpperCase() === 'POST') {
1799
- void (async () => {
1800
- const { body, error } = await readRequestBody(req, 10 * 1024 * 1024);
1801
- if (error) {
1802
- res.writeHead(413, { 'Content-Type': 'application/json; charset=utf-8' });
1803
- res.end(JSON.stringify({ error }));
1804
- return;
1805
- }
1806
- const parsed = parseJsonOrError(body);
1807
- if (parsed.error) {
1808
- res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
1809
- res.end(JSON.stringify({ error: `invalid json: ${parsed.error}` }));
1810
- return;
1811
- }
1812
-
1813
- const payload = parsed.value && typeof parsed.value === 'object' ? parsed.value : {};
1814
- const wantsStream = payload.stream === true;
1815
-
1816
- const commonHeaders = {
1817
- ...(upstream.authHeader ? { 'Authorization': upstream.authHeader } : {}),
1818
- 'X-Codexmate-Proxy': '1'
1819
- };
1820
-
1821
- const model = typeof payload.model === 'string' ? payload.model : '';
1822
- const chatBody = buildChatCompletionsBodyFromResponsesPayload(payload);
1823
- const toolTypesByName = collectResponsesToolTypesByName(payload.tools);
1824
-
1825
- if (wantsStream) {
1826
- const streamingChatBody = { ...chatBody, stream: true };
1827
- const streamed = await streamChatCompletionsAsResponsesSseWithFallbackUrls(upstream.baseUrl, 'chat/completions', {
1828
- method: 'POST',
1829
- headers: commonHeaders,
1830
- timeoutMs,
1831
- body: streamingChatBody,
1832
- res,
1833
- model,
1834
- toolTypesByName
1835
- });
1836
- if (!streamed.ok) {
1837
- if (!res.headersSent) {
1838
- res.writeHead(streamed.status && streamed.status >= 400 ? streamed.status : 502, { 'Content-Type': 'application/json; charset=utf-8' });
1839
- res.end(streamed.bodyText || JSON.stringify({ error: streamed.error || 'proxy request failed' }));
1840
- } else if (!res.writableEnded) {
1841
- writeSse(res, 'response.failed', { type: 'response.failed', error: streamed.error || streamed.bodyText || 'proxy request failed' });
1842
- writeSse(res, 'done', '[DONE]');
1843
- res.end();
1844
- }
1845
- }
1846
- return;
1847
- }
1848
-
1849
- const upstreamResponses = await proxyRequestJsonWithFallbackUrls(upstream.baseUrl, 'responses', {
1850
- method: 'POST',
1851
- headers: commonHeaders,
1852
- timeoutMs,
1853
- body: { ...payload, stream: false }
1854
- });
1855
-
1856
- // 优先走上游 /responses(如果支持)。若上游报错且不是“端点不支持”,则直接透传错误。
1857
- if (upstreamResponses.ok && upstreamResponses.status >= 200 && upstreamResponses.status < 300) {
1858
- const json = parseJsonOrError(upstreamResponses.bodyText);
1859
- if (json.error) {
1860
- res.writeHead(502, { 'Content-Type': 'application/json; charset=utf-8' });
1861
- res.end(JSON.stringify({ error: `Upstream JSON parse failed: ${json.error}` }));
1862
- return;
1863
- }
1864
- const responsesPayload = ensureResponseMetadata(json.value);
1865
- if (wantsStream) {
1866
- res.writeHead(200, {
1867
- 'Content-Type': 'text/event-stream; charset=utf-8',
1868
- 'Cache-Control': 'no-cache',
1869
- 'Connection': 'keep-alive',
1870
- 'X-Accel-Buffering': 'no'
1871
- });
1872
- sendResponsesSse(res, responsesPayload);
1873
- res.end();
1874
- return;
1875
- }
1876
- res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
1877
- res.end(JSON.stringify(responsesPayload));
1878
- return;
1879
- }
1880
-
1881
- if (upstreamResponses.ok && upstreamResponses.status >= 400) {
1882
- if (!shouldFallbackFromUpstreamResponses(upstreamResponses.status, upstreamResponses.bodyText)) {
1883
- res.writeHead(upstreamResponses.status, { 'Content-Type': 'application/json; charset=utf-8' });
1884
- res.end(upstreamResponses.bodyText || JSON.stringify({ error: 'Upstream error' }));
1885
- return;
1886
- }
1887
- // fallthrough to chat/completions conversion
1888
- }
1889
-
1890
- if (!upstreamResponses.ok) {
1891
- if (!shouldFallbackFromUpstreamResponsesFailure(upstreamResponses.error)) {
1892
- res.writeHead(502, { 'Content-Type': 'application/json; charset=utf-8' });
1893
- res.end(JSON.stringify({ error: upstreamResponses.error || 'Upstream request failed' }));
1894
- return;
1895
- }
1896
- // Some OpenAI-compatible gateways accept /responses but never complete it.
1897
- // Treat that as an unsupported Responses endpoint and try the chat fallback.
1898
- }
1899
-
1900
- const upstreamChat = await proxyRequestJsonWithFallbackUrls(upstream.baseUrl, 'chat/completions', {
1901
- method: 'POST',
1902
- headers: commonHeaders,
1903
- timeoutMs,
1904
- body: chatBody
1905
- });
1906
- if (!upstreamChat.ok) {
1907
- res.writeHead(502, { 'Content-Type': 'application/json; charset=utf-8' });
1908
- res.end(JSON.stringify({ error: upstreamChat.error || 'proxy request failed' }));
1909
- return;
1910
- }
1911
-
1912
- if (upstreamChat.status >= 400) {
1913
- res.writeHead(upstreamChat.status, { 'Content-Type': 'application/json; charset=utf-8' });
1914
- res.end(upstreamChat.bodyText || JSON.stringify({ error: 'Upstream error' }));
1915
- return;
1916
- }
1917
-
1918
- const chatJson = parseJsonOrError(upstreamChat.bodyText);
1919
- if (chatJson.error) {
1920
- res.writeHead(502, { 'Content-Type': 'application/json; charset=utf-8' });
1921
- res.end(JSON.stringify({ error: `invalid upstream response: ${chatJson.error}` }));
1922
- return;
1923
- }
1924
-
1925
- const responsesPayload = buildResponsesPayloadFromChatCompletion(chatJson.value, model, { toolTypesByName });
1926
-
1927
- if (wantsStream) {
1928
- res.writeHead(200, {
1929
- 'Content-Type': 'text/event-stream; charset=utf-8',
1930
- 'Cache-Control': 'no-cache',
1931
- 'Connection': 'keep-alive',
1932
- 'X-Accel-Buffering': 'no'
1933
- });
1934
- sendResponsesSse(res, responsesPayload);
1935
- res.end();
1936
- return;
1937
- }
1938
- res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
1939
- res.end(JSON.stringify(responsesPayload));
1940
- })();
1941
- return;
1942
- }
1943
-
1944
- const suffix = incomingPath === '/v1'
1945
- ? ''
1946
- : incomingPath.replace(/^\/v1\/?/, '');
1947
- const targetBase = joinApiUrl(upstream.baseUrl, suffix);
1948
- if (!targetBase) {
1949
- const body = JSON.stringify({ error: 'failed to build upstream URL' });
1950
- res.writeHead(500, {
1951
- 'Content-Type': 'application/json; charset=utf-8',
1952
- 'Content-Length': Buffer.byteLength(body, 'utf-8')
1953
- });
1954
- res.end(body, 'utf-8');
1955
- return;
1956
- }
1957
-
1958
- let targetUrl;
1959
- try {
1960
- targetUrl = new URL(targetBase);
1961
- targetUrl.search = parsedIncoming.search || '';
1962
- } catch (e) {
1963
- const body = JSON.stringify({ error: `invalid upstream URL: ${e.message}` });
1964
- res.writeHead(500, {
1965
- 'Content-Type': 'application/json; charset=utf-8',
1966
- 'Content-Length': Buffer.byteLength(body, 'utf-8')
1967
- });
1968
- res.end(body, 'utf-8');
1969
- return;
1970
- }
1971
-
1972
- const requestHeaders = { ...req.headers };
1973
- delete requestHeaders.host;
1974
- delete requestHeaders.connection;
1975
- delete requestHeaders['content-length'];
1976
- if (upstream.authHeader) {
1977
- requestHeaders.authorization = upstream.authHeader;
1978
- }
1979
- requestHeaders['x-codexmate-proxy'] = '1';
1980
- if (!requestHeaders['x-forwarded-for'] && req.socket && req.socket.remoteAddress) {
1981
- requestHeaders['x-forwarded-for'] = req.socket.remoteAddress;
1982
- }
1983
-
1984
- const transport = targetUrl.protocol === 'https:' ? https : http;
1985
- const upstreamReq = transport.request({
1986
- protocol: targetUrl.protocol,
1987
- hostname: targetUrl.hostname,
1988
- port: targetUrl.port || (targetUrl.protocol === 'https:' ? 443 : 80),
1989
- method: req.method || 'GET',
1990
- path: `${targetUrl.pathname}${targetUrl.search}`,
1991
- headers: requestHeaders,
1992
- agent: targetUrl.protocol === 'https:' ? HTTPS_KEEP_ALIVE_AGENT : HTTP_KEEP_ALIVE_AGENT
1993
- }, (upstreamRes) => {
1994
- const responseHeaders = { ...upstreamRes.headers };
1995
- delete responseHeaders.connection;
1996
- res.writeHead(upstreamRes.statusCode || 502, responseHeaders);
1997
- upstreamRes.pipe(res);
1998
- });
1999
-
2000
- upstreamReq.setTimeout(timeoutMs, () => {
2001
- upstreamReq.destroy(new Error(`upstream timeout (${timeoutMs}ms)`));
2002
- });
2003
-
2004
- upstreamReq.on('error', (err) => {
2005
- if (res.headersSent) {
2006
- try { res.destroy(err); } catch (_) {}
2007
- return;
2008
- }
2009
- const body = JSON.stringify({ error: `proxy request failed: ${err.message}` });
2010
- res.writeHead(502, {
2011
- 'Content-Type': 'application/json; charset=utf-8',
2012
- 'Content-Length': Buffer.byteLength(body, 'utf-8')
2013
- });
2014
- res.end(body, 'utf-8');
2015
- });
2016
-
2017
- req.pipe(upstreamReq);
2018
- });
2019
-
2020
- server.on('connection', (socket) => {
2021
- connections.add(socket);
2022
- socket.on('close', () => connections.delete(socket));
2023
- });
2024
-
2025
- return new Promise((resolve, reject) => {
2026
- server.once('error', reject);
2027
- server.listen(settings.port, settings.host, () => {
2028
- server.removeListener('error', reject);
2029
- resolve({
2030
- server,
2031
- connections,
2032
- settings,
2033
- upstream,
2034
- startedAt: toIsoTime(Date.now()),
2035
- listenUrl: buildProxyListenUrl(settings)
2036
- });
2037
- });
2038
- });
2039
- }
2040
-
2041
- async function startBuiltinProxyRuntime(payload = {}) {
2042
- if (runtime) {
2043
- return {
2044
- error: '内建代理已在运行',
2045
- runtime: {
2046
- listenUrl: runtime.listenUrl,
2047
- upstreamProvider: runtime.upstream.providerName
2048
- }
2049
- };
2050
- }
2051
-
2052
- const saveResult = saveBuiltinProxySettings(payload);
2053
- if (saveResult.error) {
2054
- return { error: saveResult.error };
2055
- }
2056
- const settings = saveResult.settings;
2057
- const upstream = resolveBuiltinProxyUpstream(settings);
2058
- if (upstream.error) {
2059
- return { error: upstream.error };
2060
- }
2061
-
2062
- try {
2063
- runtime = await createBuiltinProxyServer(settings, upstream);
2064
- return {
2065
- success: true,
2066
- running: true,
2067
- listenUrl: runtime.listenUrl,
2068
- upstreamProvider: upstream.providerName,
2069
- settings
2070
- };
2071
- } catch (e) {
2072
- return { error: `启动内建代理失败: ${e.message}` };
2073
- }
2074
- }
2075
-
2076
- async function stopBuiltinProxyRuntime() {
2077
- if (!runtime) {
2078
- return { success: true, running: false };
2079
- }
2080
- const currentRuntime = runtime;
2081
- runtime = null;
2082
-
2083
- await new Promise((resolve) => {
2084
- let settled = false;
2085
- const finish = () => {
2086
- if (settled) return;
2087
- settled = true;
2088
- resolve();
2089
- };
2090
-
2091
- currentRuntime.server.close(() => finish());
2092
- setTimeout(() => finish(), 1000);
2093
- });
2094
-
2095
- for (const socket of currentRuntime.connections) {
2096
- try { socket.destroy(); } catch (_) {}
2097
- }
2098
- currentRuntime.connections.clear();
2099
-
2100
- return {
2101
- success: true,
2102
- running: false
2103
- };
2104
- }
2105
-
2106
- function getBuiltinProxyStatus() {
2107
- const settings = readBuiltinProxySettings();
2108
- return {
2109
- running: !!runtime,
2110
- settings,
2111
- runtime: runtime
2112
- ? {
2113
- provider: BUILTIN_PROXY_PROVIDER_NAME,
2114
- startedAt: runtime.startedAt,
2115
- listenUrl: runtime.listenUrl,
2116
- upstreamProvider: runtime.upstream.providerName,
2117
- upstreamBaseUrl: runtime.upstream.baseUrl
2118
- }
2119
- : null
2120
- };
2121
- }
2122
-
2123
- return {
2124
- canListenPort,
2125
- findAvailablePort,
2126
- normalizeBuiltinProxySettings,
2127
- readBuiltinProxySettings,
2128
- resolveBuiltinProxyProviderName,
2129
- saveBuiltinProxySettings,
2130
- buildProxyListenUrl,
2131
- buildBuiltinProxyProviderBaseUrl,
2132
- removePersistedBuiltinProxyProviderFromConfig,
2133
- hasCodexConfigReadyForProxy,
2134
- resolveBuiltinProxyUpstream,
2135
- createBuiltinProxyServer,
2136
- startBuiltinProxyRuntime,
2137
- stopBuiltinProxyRuntime,
2138
- getBuiltinProxyStatus
2139
- };
2140
- }
2141
-
2142
- module.exports = {
2143
- createBuiltinProxyRuntimeController
2144
- };
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 isTransientNetworkError(error) {
131
+ const text = String(error || '').trim();
132
+ if (!text) return false;
133
+ if (/socket hang up/i.test(text)) return true;
134
+ if (/ECONNRESET|ECONNREFUSED|EPIPE|EPROTO|ETIMEDOUT/i.test(text)) return true;
135
+ if (/EAI_AGAIN/i.test(text)) return true;
136
+ if (/UND_ERR_SOCKET/i.test(text)) return true;
137
+ if (/disconnected before|secure tls|tls handshake/i.test(text)) return true;
138
+ return false;
139
+ }
140
+
141
+ const TRANSIENT_RETRY_DELAYS_MS = [200, 600, 1200];
142
+
143
+ async function retryTransientRequest(executor) {
144
+ let lastResult = null;
145
+ for (let attempt = 0; attempt <= TRANSIENT_RETRY_DELAYS_MS.length; attempt += 1) {
146
+ if (attempt > 0) {
147
+ const delay = TRANSIENT_RETRY_DELAYS_MS[attempt - 1];
148
+ // eslint-disable-next-line no-await-in-loop
149
+ await new Promise((r) => {
150
+ const t = setTimeout(r, delay);
151
+ if (typeof t.unref === 'function') t.unref();
152
+ });
153
+ }
154
+ // eslint-disable-next-line no-await-in-loop
155
+ const result = await executor(attempt);
156
+ lastResult = result;
157
+ if (!result) return result;
158
+ if (result.ok) return result;
159
+ if (result.retry) return result;
160
+ if (result.status && result.status > 0) return result;
161
+ if (!result.retryTransient && !isTransientNetworkError(result.error)) return result;
162
+ }
163
+ return lastResult;
164
+ }
165
+
166
+ function proxyRequestJson(targetUrl, options = {}) {
167
+ const parsed = new URL(targetUrl);
168
+ const transport = parsed.protocol === 'https:' ? https : http;
169
+ const bodyText = options.body ? JSON.stringify(options.body) : '';
170
+ const headers = {
171
+ 'Accept': 'application/json',
172
+ ...(options.body ? { 'Content-Type': 'application/json' } : {}),
173
+ ...(options.headers || {})
174
+ };
175
+ if (options.body) {
176
+ headers['Content-Length'] = Buffer.byteLength(bodyText, 'utf-8');
177
+ }
178
+ const timeoutMs = Number.isFinite(options.timeoutMs)
179
+ ? Math.max(1000, Number(options.timeoutMs))
180
+ : 30000;
181
+
182
+ return new Promise((resolve) => {
183
+ let settled = false;
184
+ const finish = (value) => {
185
+ if (settled) return;
186
+ settled = true;
187
+ resolve(value);
188
+ };
189
+ const req = transport.request({
190
+ protocol: parsed.protocol,
191
+ hostname: parsed.hostname,
192
+ port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
193
+ method: options.method || 'GET',
194
+ path: `${parsed.pathname}${parsed.search}`,
195
+ headers,
196
+ agent: parsed.protocol === 'https:' ? HTTPS_KEEP_ALIVE_AGENT : HTTP_KEEP_ALIVE_AGENT
197
+ }, (upstreamRes) => {
198
+ const chunks = [];
199
+ upstreamRes.on('data', (chunk) => chunk && chunks.push(chunk));
200
+ upstreamRes.on('end', () => {
201
+ const text = chunks.length ? Buffer.concat(chunks).toString('utf-8') : '';
202
+ finish({
203
+ ok: true,
204
+ status: upstreamRes.statusCode || 0,
205
+ headers: upstreamRes.headers || {},
206
+ bodyText: text
207
+ });
208
+ });
209
+ });
210
+ req.setTimeout(timeoutMs, () => {
211
+ try { req.destroy(new Error('timeout')); } catch (_) {}
212
+ finish({ ok: false, error: 'timeout' });
213
+ });
214
+ req.on('error', (err) => finish({ ok: false, error: err && err.message ? err.message : 'request failed' }));
215
+ if (bodyText) {
216
+ req.write(bodyText);
217
+ }
218
+ req.end();
219
+ });
220
+ }
221
+
222
+ function buildUpstreamUrlCandidates(baseUrl, pathSuffix) {
223
+ const safeSuffix = String(pathSuffix || '').replace(/^\/+/, '');
224
+ const candidates = [];
225
+ const push = (url) => {
226
+ if (url && !candidates.includes(url)) {
227
+ candidates.push(url);
228
+ }
229
+ };
230
+ push(joinApiUrl(baseUrl, safeSuffix));
231
+ const trimmed = normalizeBaseUrl(baseUrl);
232
+ if (trimmed && safeSuffix) {
233
+ push(`${trimmed}/${safeSuffix}`);
234
+ }
235
+ return candidates;
236
+ }
237
+
238
+ async function proxyRequestJsonWithFallbackUrls(baseUrl, pathSuffix, options = {}) {
239
+ const urls = buildUpstreamUrlCandidates(baseUrl, pathSuffix);
240
+ if (urls.length === 0) {
241
+ return { ok: false, error: 'failed to build upstream URL' };
242
+ }
243
+ let lastResult = null;
244
+ for (let index = 0; index < urls.length; index += 1) {
245
+ const result = await retryTransientRequest(() => proxyRequestJson(urls[index], options));
246
+ lastResult = result;
247
+ if (!result.ok) {
248
+ return result;
249
+ }
250
+ if (!(result.status === 404 || result.status === 405)) {
251
+ return result;
252
+ }
253
+ }
254
+ return lastResult || { ok: false, error: 'failed to build upstream URL' };
255
+ }
256
+
257
+ function stringifyJsonValue(value, fallback = '') {
258
+ if (typeof value === 'string') return value;
259
+ if (value == null) return fallback;
260
+ try {
261
+ return JSON.stringify(value);
262
+ } catch (_) {
263
+ return fallback;
264
+ }
265
+ }
266
+
267
+ function parseJsonValueOrNull(value) {
268
+ if (typeof value !== 'string') return null;
269
+ const text = value.trim();
270
+ if (!text) return null;
271
+ try {
272
+ return JSON.parse(text);
273
+ } catch (_) {
274
+ return null;
275
+ }
276
+ }
277
+
278
+ function normalizeChatUsageToResponsesUsage(usage) {
279
+ if (!usage || typeof usage !== 'object' || Array.isArray(usage)) return undefined;
280
+ const pickNumber = (...keys) => {
281
+ for (const key of keys) {
282
+ if (Number.isFinite(usage[key])) return usage[key];
283
+ }
284
+ return undefined;
285
+ };
286
+ const inputTokens = pickNumber('input_tokens', 'prompt_tokens');
287
+ const outputTokens = pickNumber('output_tokens', 'completion_tokens');
288
+ const totalTokens = pickNumber('total_tokens');
289
+ const result = {};
290
+ if (inputTokens != null) result.input_tokens = inputTokens;
291
+ if (outputTokens != null) result.output_tokens = outputTokens;
292
+ if (totalTokens != null) result.total_tokens = totalTokens;
293
+ if (usage.input_tokens_details && typeof usage.input_tokens_details === 'object') {
294
+ result.input_tokens_details = usage.input_tokens_details;
295
+ } else if (usage.prompt_tokens_details && typeof usage.prompt_tokens_details === 'object') {
296
+ result.input_tokens_details = usage.prompt_tokens_details;
297
+ }
298
+ if (usage.output_tokens_details && typeof usage.output_tokens_details === 'object') {
299
+ result.output_tokens_details = usage.output_tokens_details;
300
+ } else if (usage.completion_tokens_details && typeof usage.completion_tokens_details === 'object') {
301
+ result.output_tokens_details = usage.completion_tokens_details;
302
+ }
303
+ return Object.keys(result).length > 0 ? result : usage;
304
+ }
305
+
306
+ function mapChatFinishReasonToResponses(choice) {
307
+ const finishReason = choice && typeof choice === 'object' && typeof choice.finish_reason === 'string'
308
+ ? choice.finish_reason
309
+ : '';
310
+ if (finishReason === 'length') {
311
+ return { status: 'incomplete', incomplete_details: { reason: 'max_output_tokens' } };
312
+ }
313
+ if (finishReason === 'content_filter') {
314
+ return { status: 'incomplete', incomplete_details: { reason: 'content_filter' } };
315
+ }
316
+ return { status: 'completed' };
317
+ }
318
+
319
+ function normalizeChatMessageContentToResponsesContent(content, refusal = '') {
320
+ const blocks = [];
321
+ const pushText = (text) => {
322
+ if (typeof text === 'string' && text) {
323
+ blocks.push({ type: 'output_text', text });
324
+ }
325
+ };
326
+ if (typeof content === 'string') {
327
+ pushText(content);
328
+ } else if (Array.isArray(content)) {
329
+ for (const item of content) {
330
+ if (!item) continue;
331
+ if (typeof item === 'string') {
332
+ pushText(item);
333
+ continue;
334
+ }
335
+ if (typeof item !== 'object') continue;
336
+ const type = typeof item.type === 'string' ? item.type : '';
337
+ if ((type === 'text' || type === 'output_text') && typeof item.text === 'string') {
338
+ pushText(item.text);
339
+ continue;
340
+ }
341
+ if (typeof item.content === 'string') {
342
+ pushText(item.content);
343
+ }
344
+ }
345
+ }
346
+ if (typeof refusal === 'string' && refusal) {
347
+ blocks.push({ type: 'refusal', refusal });
348
+ }
349
+ return blocks;
350
+ }
351
+
352
+ function buildResponsesPayloadFromChatCompletion(payload, fallbackModel = '', options = {}) {
353
+ const base = payload && typeof payload === 'object' && !Array.isArray(payload) ? payload : {};
354
+ const choice = Array.isArray(base.choices) ? base.choices[0] : null;
355
+ const message = choice && typeof choice === 'object' && choice.message && typeof choice.message === 'object'
356
+ ? choice.message
357
+ : {};
358
+ const output = [];
359
+ const messageContent = normalizeChatMessageContentToResponsesContent(message.content, message.refusal);
360
+ if (messageContent.length > 0 || !Array.isArray(message.tool_calls) || message.tool_calls.length === 0) {
361
+ output.push({
362
+ type: 'message',
363
+ role: 'assistant',
364
+ content: messageContent.length > 0 ? messageContent : [{ type: 'output_text', text: '' }]
365
+ });
366
+ }
367
+ if (Array.isArray(message.tool_calls)) {
368
+ for (const toolCall of message.tool_calls) {
369
+ const item = buildResponsesToolCallItemFromChatToolCall(toolCall, options.toolTypesByName || {});
370
+ if (item) output.push(item);
371
+ }
372
+ }
373
+ const finish = mapChatFinishReasonToResponses(choice);
374
+ return ensureResponseMetadata({
375
+ id: typeof base.id === 'string' ? base.id : undefined,
376
+ model: typeof base.model === 'string' ? base.model : fallbackModel,
377
+ status: finish.status,
378
+ ...(finish.incomplete_details ? { incomplete_details: finish.incomplete_details } : {}),
379
+ output,
380
+ usage: normalizeChatUsageToResponsesUsage(base.usage)
381
+ });
382
+ }
383
+
384
+ function isRecord(value) {
385
+ return !!value && typeof value === 'object' && !Array.isArray(value);
386
+ }
387
+
388
+ function asTrimmedString(value) {
389
+ return typeof value === 'string' ? value.trim() : '';
390
+ }
391
+
392
+ function cloneJsonValue(value) {
393
+ if (Array.isArray(value)) return value.map((item) => cloneJsonValue(item));
394
+ if (isRecord(value)) {
395
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, cloneJsonValue(item)]));
396
+ }
397
+ return value;
398
+ }
399
+
400
+ function normalizeResponsesToolOutput(value) {
401
+ if (typeof value === 'string') return value;
402
+ if (value == null) return '';
403
+ return stringifyJsonValue(value, '');
404
+ }
405
+
406
+ function normalizeOpenAiToolArguments(value) {
407
+ if (typeof value === 'string') return value;
408
+ if (value == null) return '{}';
409
+ return stringifyJsonValue(value, '{}');
410
+ }
411
+
412
+ function normalizeInputFileBlock(item) {
413
+ if (!isRecord(item)) return null;
414
+ const file = isRecord(item.file) ? item.file : item;
415
+ const out = {};
416
+ const fileId = asTrimmedString(file.file_id || file.id);
417
+ const filename = asTrimmedString(file.filename || file.name);
418
+ const fileData = asTrimmedString(file.file_data || file.data);
419
+ const mimeType = asTrimmedString(file.mime_type || file.media_type);
420
+ if (fileId) out.file_id = fileId;
421
+ if (filename) out.filename = filename;
422
+ if (fileData) out.file_data = fileData;
423
+ if (mimeType) out.mime_type = mimeType;
424
+ return Object.keys(out).length > 0 ? out : null;
425
+ }
426
+
427
+ function normalizeResponsesContentBlockForChat(item) {
428
+ if (typeof item === 'string') return item.trim() ? item : null;
429
+ if (!isRecord(item)) return null;
430
+
431
+ const type = asTrimmedString(item.type).toLowerCase();
432
+ if (!type) {
433
+ const text = asTrimmedString(item.text || item.content || item.output_text);
434
+ return text ? { type: 'text', text } : null;
435
+ }
436
+
437
+ if (type === 'input_text' || type === 'output_text' || type === 'text' || type === 'summary_text' || type === 'reasoning_text') {
438
+ const text = typeof item.text === 'string' ? item.text : asTrimmedString(item.content || item.output_text);
439
+ return text ? { type: 'text', text } : null;
440
+ }
441
+
442
+ if (type === 'refusal' && typeof item.refusal === 'string') {
443
+ return item.refusal ? { type: 'text', text: item.refusal } : null;
444
+ }
445
+
446
+ if (type === 'input_image') {
447
+ const raw = item.image_url != null ? item.image_url : (item.url != null ? item.url : item.imageUrl);
448
+ if (raw === undefined) return null;
449
+ return {
450
+ type: 'image_url',
451
+ image_url: typeof raw === 'string' ? { url: raw } : cloneJsonValue(raw)
452
+ };
453
+ }
454
+
455
+ if (type === 'image_url' && item.image_url !== undefined) {
456
+ return { type: 'image_url', image_url: item.image_url };
457
+ }
458
+
459
+ if (type === 'input_audio') {
460
+ if (item.input_audio !== undefined) return { type: 'input_audio', input_audio: item.input_audio };
461
+ if (item.data !== undefined || item.format !== undefined) {
462
+ return { type: 'input_audio', input_audio: { data: item.data, format: item.format } };
463
+ }
464
+ return null;
465
+ }
466
+
467
+ if (type === 'input_file' || type === 'file') {
468
+ const file = normalizeInputFileBlock(item);
469
+ return file ? { type: 'file', file } : null;
470
+ }
471
+
472
+ if (type === 'reasoning' || type === 'thinking' || type === 'redacted_reasoning') {
473
+ const text = asTrimmedString(item.text || item.content);
474
+ return text ? { type: 'text', text } : null;
475
+ }
476
+
477
+ return cloneJsonValue(item);
478
+ }
479
+
480
+ function toOpenAiMessageContent(content) {
481
+ if (typeof content === 'string') return content;
482
+ if (!Array.isArray(content)) {
483
+ if (isRecord(content)) {
484
+ const single = normalizeResponsesContentBlockForChat(content);
485
+ if (!single) return '';
486
+ return typeof single === 'string' ? single : [single];
487
+ }
488
+ return '';
489
+ }
490
+
491
+ const blocks = content
492
+ .map((item) => normalizeResponsesContentBlockForChat(item))
493
+ .filter((item) => !!item);
494
+
495
+ if (blocks.length === 0) return '';
496
+ if (blocks.length === 1 && typeof blocks[0] === 'string') return blocks[0];
497
+ return blocks;
498
+ }
499
+
500
+ const RESPONSES_TOOL_CALL_INPUT_TYPES = new Set(['function_call', 'custom_tool_call', 'mcp_tool_call', 'local_shell_call']);
501
+ const RESPONSES_TOOL_CALL_OUTPUT_TYPES = new Set(['function_call_output', 'custom_tool_call_output', 'mcp_tool_call_output', 'tool_search_output', 'local_shell_call_output']);
502
+
503
+ function stripOrphanedResponsesToolOutputs(input) {
504
+ if (!Array.isArray(input)) return input;
505
+ const seenToolCallIds = new Set();
506
+ const sanitized = [];
507
+ for (const item of input) {
508
+ if (!isRecord(item)) {
509
+ sanitized.push(item);
510
+ continue;
511
+ }
512
+ const type = asTrimmedString(item.type).toLowerCase();
513
+ if (RESPONSES_TOOL_CALL_INPUT_TYPES.has(type)) {
514
+ const callId = asTrimmedString(item.call_id || item.id);
515
+ if (callId) seenToolCallIds.add(callId);
516
+ sanitized.push(item);
517
+ continue;
518
+ }
519
+ if (RESPONSES_TOOL_CALL_OUTPUT_TYPES.has(type)) {
520
+ const callId = asTrimmedString(item.call_id || item.id);
521
+ if (!callId || !seenToolCallIds.has(callId)) continue;
522
+ sanitized.push(item);
523
+ continue;
524
+ }
525
+ sanitized.push(item);
526
+ }
527
+ return sanitized;
528
+ }
529
+
530
+ function normalizeFreeformToolArguments(value) {
531
+ if (typeof value === 'string') return stringifyJsonValue({ input: value }, '{"input":""}');
532
+ if (value == null) return '{"input":""}';
533
+ if (isRecord(value) && Object.prototype.hasOwnProperty.call(value, 'input')) {
534
+ return stringifyJsonValue(value, '{"input":""}');
535
+ }
536
+ return stringifyJsonValue({ input: normalizeResponsesToolOutput(value) }, '{"input":""}');
537
+ }
538
+
539
+ function toOpenAiToolCall(item, fallbackIndex) {
540
+ if (!isRecord(item)) return null;
541
+ const callId = asTrimmedString(item.call_id || item.id) || `call_${crypto.randomBytes(8).toString('hex')}_${fallbackIndex}`;
542
+ const name = asTrimmedString(item.name) || asTrimmedString(item.server_label);
543
+ if (!name) return null;
544
+ const type = asTrimmedString(item.type).toLowerCase();
545
+ const rawArguments = item.arguments != null ? item.arguments : item.input;
546
+ const args = type === 'custom_tool_call' && item.arguments == null
547
+ ? normalizeFreeformToolArguments(rawArguments)
548
+ : normalizeOpenAiToolArguments(rawArguments);
549
+ return {
550
+ id: callId,
551
+ type: 'function',
552
+ function: {
553
+ name,
554
+ arguments: args
555
+ }
556
+ };
557
+ }
558
+
559
+ function hasOpenAiMessageContent(content) {
560
+ return typeof content === 'string'
561
+ ? content.trim().length > 0
562
+ : Array.isArray(content) && content.length > 0;
563
+ }
564
+
565
+ function normalizeResponsesInputToChatMessages(input) {
566
+ // 参考 metapi 的 Responses → Chat 桥接:聚合连续 tool calls、丢弃孤儿 tool outputs,
567
+ // 并保留 reasoning / richer content blocks / developer-role compatibility。
568
+ const messages = [];
569
+ const normalizedInput = stripOrphanedResponsesToolOutputs(input);
570
+ let functionCallIndex = 0;
571
+ let pendingToolCalls = [];
572
+ const emittedToolCallIds = new Set();
573
+
574
+ const flushPendingToolCalls = () => {
575
+ if (pendingToolCalls.length <= 0) return;
576
+ for (const toolCall of pendingToolCalls) {
577
+ const callId = asTrimmedString(toolCall.id);
578
+ if (callId) emittedToolCallIds.add(callId);
579
+ }
580
+ messages.push({
581
+ role: 'assistant',
582
+ content: null,
583
+ tool_calls: pendingToolCalls
584
+ });
585
+ pendingToolCalls = [];
586
+ };
587
+
588
+ const pushToolOutputMessage = (callIdRaw, outputRaw) => {
589
+ const toolCallId = asTrimmedString(callIdRaw);
590
+ if (!toolCallId) return;
591
+ messages.push({
592
+ role: 'tool',
593
+ tool_call_id: toolCallId,
594
+ content: normalizeResponsesToolOutput(outputRaw)
595
+ });
596
+ };
597
+
598
+ const processInputItem = (item) => {
599
+ if (typeof item === 'string') {
600
+ flushPendingToolCalls();
601
+ const text = item.trim();
602
+ if (text) messages.push({ role: 'user', content: text });
603
+ return;
604
+ }
605
+ if (!isRecord(item)) return;
606
+
607
+ const itemType = asTrimmedString(item.type).toLowerCase();
608
+ if (itemType === 'function_call' || itemType === 'custom_tool_call') {
609
+ const toolCall = toOpenAiToolCall(item, functionCallIndex);
610
+ functionCallIndex += 1;
611
+ if (toolCall) pendingToolCalls.push(toolCall);
612
+ return;
613
+ }
614
+
615
+ if (itemType === 'function_call_output' || itemType === 'custom_tool_call_output') {
616
+ flushPendingToolCalls();
617
+ const toolCallId = asTrimmedString(item.call_id || item.id);
618
+ if (!toolCallId || !emittedToolCallIds.has(toolCallId)) return;
619
+ pushToolOutputMessage(toolCallId, item.output != null ? item.output : item.content);
620
+ return;
621
+ }
622
+
623
+ if (itemType === 'reasoning') {
624
+ // Any non-tool-call item is a sequence boundary: keep only consecutive
625
+ // tool calls in the same assistant `tool_calls` message.
626
+ flushPendingToolCalls();
627
+ const reasoningContent = toOpenAiMessageContent(item.summary != null ? item.summary : (item.content != null ? item.content : item));
628
+ const reasoningSignature = asTrimmedString(item.encrypted_content || item.reasoning_signature);
629
+ if (!hasOpenAiMessageContent(reasoningContent) && !reasoningSignature) return;
630
+ const message = { role: 'assistant', content: reasoningContent };
631
+ if (reasoningSignature) message.reasoning_signature = reasoningSignature;
632
+ messages.push(message);
633
+ return;
634
+ }
635
+
636
+ flushPendingToolCalls();
637
+ const role = asTrimmedString(item.role).toLowerCase() || 'user';
638
+ const normalizedRole = role === 'developer' ? 'system' : role;
639
+ const content = toOpenAiMessageContent(item.content != null ? item.content : (item.input != null ? item.input : item));
640
+
641
+ if (normalizedRole === 'tool') {
642
+ const toolCallId = asTrimmedString(item.tool_call_id || item.call_id || item.id);
643
+ if (!toolCallId || !emittedToolCallIds.has(toolCallId)) return;
644
+ pushToolOutputMessage(toolCallId, item.content);
645
+ return;
646
+ }
647
+
648
+ if (!hasOpenAiMessageContent(content)) return;
649
+ const message = { role: normalizedRole, content };
650
+ const phase = asTrimmedString(item.phase);
651
+ if (phase) message.phase = phase;
652
+ messages.push(message);
653
+ };
654
+
655
+ if (typeof normalizedInput === 'string') {
656
+ const text = normalizedInput.trim();
657
+ if (text) messages.push({ role: 'user', content: text });
658
+ } else if (Array.isArray(normalizedInput)) {
659
+ for (const item of normalizedInput) processInputItem(item);
660
+ } else if (isRecord(normalizedInput)) {
661
+ processInputItem(normalizedInput);
662
+ }
663
+ flushPendingToolCalls();
664
+ return messages;
665
+ }
666
+
667
+ function normalizeFunctionToolForChat(tool) {
668
+ if (!isRecord(tool)) return null;
669
+ const sourceFn = isRecord(tool.function) ? tool.function : tool;
670
+ const name = asTrimmedString(sourceFn.name) || asTrimmedString(tool.name);
671
+ if (!name) return null;
672
+ const fn = { name };
673
+ const description = asTrimmedString(sourceFn.description) || asTrimmedString(tool.description);
674
+ if (description) fn.description = description;
675
+ if (sourceFn.parameters !== undefined) {
676
+ fn.parameters = cloneJsonValue(sourceFn.parameters);
677
+ } else if (tool.parameters !== undefined) {
678
+ fn.parameters = cloneJsonValue(tool.parameters);
679
+ }
680
+ if (typeof sourceFn.strict === 'boolean') {
681
+ fn.strict = sourceFn.strict;
682
+ } else if (typeof tool.strict === 'boolean') {
683
+ fn.strict = tool.strict;
684
+ }
685
+ return { type: 'function', function: fn };
686
+ }
687
+
688
+ function buildLocalShellToolForChat(tool) {
689
+ return {
690
+ type: 'function',
691
+ function: {
692
+ name: asTrimmedString(tool && tool.name) || 'local_shell',
693
+ description: asTrimmedString(tool && tool.description) || 'Run a local shell command and return its output.',
694
+ parameters: {
695
+ type: 'object',
696
+ properties: {
697
+ cmd: { type: 'string', description: 'Shell command to execute.' },
698
+ yield_time_ms: { type: 'number', description: 'Milliseconds to wait before yielding partial output.' },
699
+ max_output_tokens: { type: 'number', description: 'Maximum output tokens to return.' }
700
+ },
701
+ required: ['cmd'],
702
+ additionalProperties: true
703
+ }
704
+ }
705
+ };
706
+ }
707
+
708
+ function buildFreeformToolForChat(tool, fallbackName = 'custom_tool') {
709
+ return {
710
+ type: 'function',
711
+ function: {
712
+ name: asTrimmedString(tool && tool.name) || fallbackName,
713
+ description: asTrimmedString(tool && tool.description) || 'Pass raw freeform input to the local tool.',
714
+ parameters: {
715
+ type: 'object',
716
+ properties: {
717
+ input: { type: 'string', description: 'Raw tool input.' }
718
+ },
719
+ required: ['input'],
720
+ additionalProperties: false
721
+ }
722
+ }
723
+ };
724
+ }
725
+
726
+ const MAX_RESPONSES_TOOL_NAMESPACE_DEPTH = 5;
727
+
728
+ function rememberResponsesToolType(tool, target, depth = 0) {
729
+ if (!isRecord(tool) || !target || depth > MAX_RESPONSES_TOOL_NAMESPACE_DEPTH) return;
730
+ const type = asTrimmedString(tool.type).toLowerCase();
731
+ if (type === 'namespace' && Array.isArray(tool.tools)) {
732
+ for (const inner of tool.tools) rememberResponsesToolType(inner, target, depth + 1);
733
+ return;
734
+ }
735
+ const sourceFn = isRecord(tool.function) ? tool.function : tool;
736
+ const name = asTrimmedString(sourceFn.name) || asTrimmedString(tool.name);
737
+ if (!name) return;
738
+ if (type === 'local_shell') {
739
+ target[name] = 'local_shell_call';
740
+ return;
741
+ }
742
+ if (type === 'custom' || type === 'custom_tool' || (!type && name === 'apply_patch')) {
743
+ target[name] = 'custom_tool_call';
744
+ return;
745
+ }
746
+ if (type === 'function') {
747
+ target[name] = 'function_call';
748
+ }
749
+ }
750
+
751
+ function collectResponsesToolTypesByName(tools) {
752
+ const result = {};
753
+ if (!Array.isArray(tools)) return result;
754
+ for (const tool of tools) rememberResponsesToolType(tool, result);
755
+ return result;
756
+ }
757
+
758
+ function extractFreeformInputFromChatArguments(argumentsText) {
759
+ if (typeof argumentsText !== 'string') return '';
760
+ const parsed = parseJsonValueOrNull(argumentsText);
761
+ if (isRecord(parsed) && Object.prototype.hasOwnProperty.call(parsed, 'input')) {
762
+ return typeof parsed.input === 'string' ? parsed.input : normalizeResponsesToolOutput(parsed.input);
763
+ }
764
+ return argumentsText;
765
+ }
766
+
767
+ function extractLocalShellActionFromChatArguments(argumentsText) {
768
+ const parsed = parseJsonValueOrNull(argumentsText);
769
+ if (isRecord(parsed)) return cloneJsonValue(parsed);
770
+ return { cmd: typeof argumentsText === 'string' ? argumentsText : '' };
771
+ }
772
+
773
+ function buildResponsesToolCallItemFromChatToolCall(toolCall, toolTypesByName = {}) {
774
+ if (!isRecord(toolCall)) return null;
775
+ const fn = isRecord(toolCall.function) ? toolCall.function : {};
776
+ const name = asTrimmedString(fn.name);
777
+ if (!name) return null;
778
+ const callId = asTrimmedString(toolCall.id) || `call_${crypto.randomBytes(8).toString('hex')}`;
779
+ const argumentsText = typeof fn.arguments === 'string' ? fn.arguments : '';
780
+ const responseType = toolTypesByName && toolTypesByName[name] ? toolTypesByName[name] : 'function_call';
781
+ if (responseType === 'custom_tool_call') {
782
+ return {
783
+ type: 'custom_tool_call',
784
+ call_id: callId,
785
+ name,
786
+ input: extractFreeformInputFromChatArguments(argumentsText)
787
+ };
788
+ }
789
+ if (responseType === 'local_shell_call') {
790
+ return {
791
+ type: 'local_shell_call',
792
+ call_id: callId,
793
+ name,
794
+ action: extractLocalShellActionFromChatArguments(argumentsText)
795
+ };
796
+ }
797
+ return {
798
+ type: 'function_call',
799
+ call_id: callId,
800
+ name,
801
+ arguments: argumentsText
802
+ };
803
+ }
804
+
805
+ function normalizeSingleResponsesToolToChatTools(tool, depth = 0) {
806
+ if (!isRecord(tool) || depth > MAX_RESPONSES_TOOL_NAMESPACE_DEPTH) return [];
807
+ const type = asTrimmedString(tool.type).toLowerCase();
808
+ if (type === 'namespace' && Array.isArray(tool.tools)) {
809
+ return tool.tools.flatMap((inner) => normalizeSingleResponsesToolToChatTools(inner, depth + 1));
810
+ }
811
+ if (type === 'function') {
812
+ const converted = normalizeFunctionToolForChat(tool);
813
+ return converted ? [converted] : [];
814
+ }
815
+ if (type === 'local_shell') {
816
+ return [buildLocalShellToolForChat(tool)];
817
+ }
818
+ const name = asTrimmedString(tool.name);
819
+ if (type === 'custom' || type === 'custom_tool' || (!type && name === 'apply_patch')) {
820
+ return [buildFreeformToolForChat(tool, name || 'custom_tool')];
821
+ }
822
+ // Hosted Responses tools such as web_search/image_generation/computer_use
823
+ // do not have a safe Chat Completions representation. Passing them through
824
+ // as-is makes OpenAI-compatible chat gateways reject the request, so drop
825
+ // them instead of pretending the shapes are compatible.
826
+ return [];
827
+ }
828
+
829
+ function normalizeResponsesToolsToChatTools(tools) {
830
+ if (!Array.isArray(tools)) return tools;
831
+ return tools.flatMap((tool) => normalizeSingleResponsesToolToChatTools(tool));
832
+ }
833
+
834
+ function normalizeResponsesToolChoiceToChatToolChoice(toolChoice) {
835
+ if (toolChoice === undefined) return undefined;
836
+ if (typeof toolChoice === 'string') return toolChoice;
837
+ if (!isRecord(toolChoice)) return toolChoice;
838
+
839
+ const type = asTrimmedString(toolChoice.type).toLowerCase();
840
+ if (type === 'tool' || type === 'function' || type === 'custom' || type === 'custom_tool' || type === 'local_shell') {
841
+ if (isRecord(toolChoice.function) && asTrimmedString(toolChoice.function.name)) return cloneJsonValue(toolChoice);
842
+ const name = asTrimmedString(toolChoice.name) || asTrimmedString(toolChoice.server_label);
843
+ if (!name) return 'required';
844
+ return { type: 'function', function: { name } };
845
+ }
846
+ if (type === 'auto' || type === 'none' || type === 'required') return type;
847
+ return 'auto';
848
+ }
849
+
850
+ function getChatToolChoiceName(toolChoice) {
851
+ if (!isRecord(toolChoice)) return '';
852
+ if (isRecord(toolChoice.function)) return asTrimmedString(toolChoice.function.name);
853
+ return '';
854
+ }
855
+
856
+ function pruneInvalidChatToolChoice(chatBody) {
857
+ if (!isRecord(chatBody) || !Array.isArray(chatBody.tools)) return;
858
+ if (chatBody.tools.length === 0) {
859
+ delete chatBody.tools;
860
+ delete chatBody.tool_choice;
861
+ return;
862
+ }
863
+ const chosenName = getChatToolChoiceName(chatBody.tool_choice);
864
+ if (!chosenName) return;
865
+ const toolNames = new Set(chatBody.tools
866
+ .map((tool) => isRecord(tool) && isRecord(tool.function) ? asTrimmedString(tool.function.name) : '')
867
+ .filter(Boolean));
868
+ if (!toolNames.has(chosenName)) {
869
+ delete chatBody.tool_choice;
870
+ }
871
+ }
872
+
873
+ function buildChatCompletionsBodyFromResponsesPayload(payload) {
874
+ const source = isRecord(payload) ? payload : {};
875
+ const messages = normalizeResponsesInputToChatMessages(source.input);
876
+ const instructions = asTrimmedString(source.instructions);
877
+ if (instructions) {
878
+ messages.unshift({ role: 'system', content: instructions });
879
+ }
880
+
881
+ const chatBody = {
882
+ model: typeof source.model === 'string' ? source.model : '',
883
+ messages,
884
+ stream: false
885
+ };
886
+
887
+ const passthroughKeys = [
888
+ 'frequency_penalty',
889
+ 'presence_penalty',
890
+ 'stop',
891
+ 'temperature',
892
+ 'top_p',
893
+ 'tools',
894
+ 'tool_choice',
895
+ 'parallel_tool_calls',
896
+ 'logprobs',
897
+ 'top_logprobs',
898
+ 'kbs',
899
+ 'is_online',
900
+ 'user',
901
+ 'seed',
902
+ 'n',
903
+ 'modalities',
904
+ 'audio',
905
+ 'reasoning',
906
+ 'reasoning_effort',
907
+ 'service_tier'
908
+ ];
909
+ for (const key of passthroughKeys) {
910
+ if (Object.prototype.hasOwnProperty.call(source, key)) {
911
+ if (key === 'tools') {
912
+ chatBody[key] = normalizeResponsesToolsToChatTools(source[key]);
913
+ } else if (key === 'tool_choice') {
914
+ chatBody[key] = normalizeResponsesToolChoiceToChatToolChoice(source[key]);
915
+ } else {
916
+ chatBody[key] = cloneJsonValue(source[key]);
917
+ }
918
+ }
919
+ }
920
+
921
+ if (Object.prototype.hasOwnProperty.call(source, 'response_format')) {
922
+ chatBody.response_format = cloneJsonValue(source.response_format);
923
+ } else if (isRecord(source.text) && source.text.format !== undefined) {
924
+ chatBody.response_format = cloneJsonValue(source.text.format);
925
+ }
926
+ if (isRecord(source.text) && asTrimmedString(source.text.verbosity)) {
927
+ chatBody.verbosity = asTrimmedString(source.text.verbosity);
928
+ }
929
+
930
+ pruneInvalidChatToolChoice(chatBody);
931
+
932
+ if (Object.prototype.hasOwnProperty.call(source, 'max_tokens')) {
933
+ chatBody.max_tokens = source.max_tokens;
934
+ } else if (source.max_output_tokens != null) {
935
+ chatBody.max_tokens = source.max_output_tokens;
936
+ }
937
+
938
+ return chatBody;
939
+ }
940
+
941
+ function ensureResponseMetadata(payload) {
942
+ const base = payload && typeof payload === 'object' && !Array.isArray(payload) ? payload : {};
943
+ const id = typeof base.id === 'string' && base.id.trim()
944
+ ? base.id.trim()
945
+ : `resp_${crypto.randomBytes(10).toString('hex')}`;
946
+ const model = typeof base.model === 'string' ? base.model : '';
947
+ return {
948
+ object: 'response',
949
+ id,
950
+ model,
951
+ ...base
952
+ };
953
+ }
954
+
955
+ function writeSse(res, eventName, dataObj) {
956
+ if (eventName) {
957
+ res.write(`event: ${eventName}\n`);
958
+ }
959
+ if (dataObj === '[DONE]') {
960
+ res.write('data: [DONE]\n\n');
961
+ return;
962
+ }
963
+ res.write(`data: ${JSON.stringify(dataObj)}\n\n`);
964
+ }
965
+
966
+ function sendResponsesSse(res, responsePayload) {
967
+ const response = ensureResponseMetadata(responsePayload);
968
+ const responseId = response.id;
969
+ const model = response.model;
970
+ let sequence = 0;
971
+ const nextSeq = () => {
972
+ sequence += 1;
973
+ return sequence;
974
+ };
975
+
976
+ writeSse(res, 'response.created', {
977
+ type: 'response.created',
978
+ response: {
979
+ id: responseId,
980
+ model,
981
+ created_at: response.created_at
982
+ }
983
+ });
984
+
985
+ const output = Array.isArray(response.output) ? response.output : [];
986
+ for (let outputIndex = 0; outputIndex < output.length; outputIndex += 1) {
987
+ const item = output[outputIndex];
988
+ if (!item || typeof item !== 'object') continue;
989
+ const itemType = typeof item.type === 'string' ? item.type : '';
990
+ const itemId = typeof item.id === 'string' && item.id.trim()
991
+ ? item.id.trim()
992
+ : `item_${crypto.randomBytes(8).toString('hex')}`;
993
+
994
+ writeSse(res, 'response.output_item.added', {
995
+ type: 'response.output_item.added',
996
+ output_index: outputIndex,
997
+ item: { ...item, id: itemId }
998
+ });
999
+
1000
+ if (itemType === 'message') {
1001
+ const content = Array.isArray(item.content) ? item.content : [];
1002
+ for (let contentIndex = 0; contentIndex < content.length; contentIndex += 1) {
1003
+ const block = content[contentIndex];
1004
+ if (!block || typeof block !== 'object') continue;
1005
+ if (block.type !== 'output_text') continue;
1006
+ const text = typeof block.text === 'string' ? block.text : '';
1007
+ if (text) {
1008
+ writeSse(res, 'response.output_text.delta', {
1009
+ type: 'response.output_text.delta',
1010
+ item_id: itemId,
1011
+ output_index: outputIndex,
1012
+ content_index: contentIndex,
1013
+ delta: text,
1014
+ sequence_number: nextSeq()
1015
+ });
1016
+ }
1017
+ writeSse(res, 'response.output_text.done', {
1018
+ type: 'response.output_text.done',
1019
+ item_id: itemId,
1020
+ output_index: outputIndex,
1021
+ content_index: contentIndex,
1022
+ text,
1023
+ sequence_number: nextSeq()
1024
+ });
1025
+ }
1026
+ }
1027
+
1028
+ writeSse(res, 'response.output_item.done', {
1029
+ type: 'response.output_item.done',
1030
+ output_index: outputIndex,
1031
+ item: { ...item, id: itemId },
1032
+ sequence_number: nextSeq()
1033
+ });
1034
+ }
1035
+
1036
+ writeSse(res, 'response.completed', { type: 'response.completed', response });
1037
+ writeSse(res, 'done', '[DONE]');
1038
+ }
1039
+
1040
+ function appendChatStreamToolCall(target, toolCall) {
1041
+ if (!toolCall || typeof toolCall !== 'object') return;
1042
+ const index = Number.isFinite(toolCall.index) ? toolCall.index : target.length;
1043
+ if (!target[index]) {
1044
+ target[index] = {
1045
+ id: '',
1046
+ type: 'function',
1047
+ function: { name: '', arguments: '' }
1048
+ };
1049
+ }
1050
+ const current = target[index];
1051
+ if (typeof toolCall.id === 'string' && toolCall.id) current.id = toolCall.id;
1052
+ if (typeof toolCall.type === 'string' && toolCall.type) current.type = toolCall.type;
1053
+ const fn = toolCall.function && typeof toolCall.function === 'object' ? toolCall.function : null;
1054
+ if (fn) {
1055
+ if (typeof fn.name === 'string' && fn.name) current.function.name = fn.name;
1056
+ if (typeof fn.arguments === 'string') current.function.arguments += fn.arguments;
1057
+ }
1058
+ }
1059
+
1060
+ function writeChatCompletionChunkAsResponsesSse(state, chunk) {
1061
+ if (!chunk || typeof chunk !== 'object') return;
1062
+ if (typeof chunk.model === 'string' && chunk.model) {
1063
+ state.model = chunk.model;
1064
+ }
1065
+ const choices = Array.isArray(chunk.choices) ? chunk.choices : [];
1066
+ for (const choice of choices) {
1067
+ const delta = choice && choice.delta && typeof choice.delta === 'object' ? choice.delta : null;
1068
+ if (!delta) continue;
1069
+
1070
+ if (typeof delta.content === 'string' && delta.content) {
1071
+ if (!state.messageItem) {
1072
+ state.messageItem = {
1073
+ id: `msg_${crypto.randomBytes(8).toString('hex')}`,
1074
+ type: 'message',
1075
+ role: 'assistant',
1076
+ content: [{ type: 'output_text', text: '' }]
1077
+ };
1078
+ state.output.push(state.messageItem);
1079
+ state.outputStarted = true;
1080
+ beginChatStreamResponsesSse(state);
1081
+ writeSse(state.res, 'response.output_item.added', {
1082
+ type: 'response.output_item.added',
1083
+ output_index: state.output.length - 1,
1084
+ item: state.messageItem
1085
+ });
1086
+ }
1087
+ state.messageText += delta.content;
1088
+ state.messageItem.content[0].text = state.messageText;
1089
+ writeSse(state.res, 'response.output_text.delta', {
1090
+ type: 'response.output_text.delta',
1091
+ item_id: state.messageItem.id,
1092
+ output_index: state.output.length - 1,
1093
+ content_index: 0,
1094
+ delta: delta.content,
1095
+ sequence_number: state.nextSeq()
1096
+ });
1097
+ }
1098
+
1099
+ if (Array.isArray(delta.tool_calls)) {
1100
+ for (const toolCall of delta.tool_calls) {
1101
+ appendChatStreamToolCall(state.toolCalls, toolCall);
1102
+ }
1103
+ }
1104
+ }
1105
+ }
1106
+
1107
+ function stopChatStreamHeartbeat(state) {
1108
+ if (!state || !state.heartbeatTimer) return;
1109
+ clearInterval(state.heartbeatTimer);
1110
+ state.heartbeatTimer = null;
1111
+ }
1112
+
1113
+ function startChatStreamHeartbeat(state) {
1114
+ if (!state || state.heartbeatTimer) return;
1115
+ const timer = setInterval(() => {
1116
+ if (state.finished) {
1117
+ stopChatStreamHeartbeat(state);
1118
+ return;
1119
+ }
1120
+ const target = state.res;
1121
+ if (!target || target.writableEnded || target.destroyed) {
1122
+ stopChatStreamHeartbeat(state);
1123
+ return;
1124
+ }
1125
+ try { target.write(': keepalive\n\n'); } catch (_) {}
1126
+ }, 15000);
1127
+ if (typeof timer.unref === 'function') timer.unref();
1128
+ state.heartbeatTimer = timer;
1129
+ }
1130
+
1131
+ function finishChatStreamResponsesSse(state) {
1132
+ if (state.finished) return;
1133
+ beginChatStreamResponsesSse(state);
1134
+ state.finished = true;
1135
+ stopChatStreamHeartbeat(state);
1136
+
1137
+ if (state.messageItem) {
1138
+ const outputIndex = state.output.indexOf(state.messageItem);
1139
+ writeSse(state.res, 'response.output_text.done', {
1140
+ type: 'response.output_text.done',
1141
+ item_id: state.messageItem.id,
1142
+ output_index: outputIndex,
1143
+ content_index: 0,
1144
+ text: state.messageText,
1145
+ sequence_number: state.nextSeq()
1146
+ });
1147
+ writeSse(state.res, 'response.output_item.done', {
1148
+ type: 'response.output_item.done',
1149
+ output_index: outputIndex,
1150
+ item: state.messageItem,
1151
+ sequence_number: state.nextSeq()
1152
+ });
1153
+ }
1154
+
1155
+ for (const toolCall of state.toolCalls) {
1156
+ if (!toolCall) continue;
1157
+ const item = buildResponsesToolCallItemFromChatToolCall(toolCall, state.toolTypesByName || {});
1158
+ if (!item) continue;
1159
+ const outputIndex = state.output.length;
1160
+ state.output.push(item);
1161
+ state.outputStarted = true;
1162
+ writeSse(state.res, 'response.output_item.added', {
1163
+ type: 'response.output_item.added',
1164
+ output_index: outputIndex,
1165
+ item
1166
+ });
1167
+ writeSse(state.res, 'response.output_item.done', {
1168
+ type: 'response.output_item.done',
1169
+ output_index: outputIndex,
1170
+ item,
1171
+ sequence_number: state.nextSeq()
1172
+ });
1173
+ }
1174
+
1175
+ const response = ensureResponseMetadata({
1176
+ id: state.responseId,
1177
+ model: state.model,
1178
+ created_at: state.createdAt,
1179
+ status: 'completed',
1180
+ output: state.output
1181
+ });
1182
+ writeSse(state.res, 'response.completed', { type: 'response.completed', response });
1183
+ writeSse(state.res, 'done', '[DONE]');
1184
+ state.res.end();
1185
+ }
1186
+
1187
+ function failResponsesSseRaw(res, message) {
1188
+ if (!res || res.writableEnded || res.destroyed) return;
1189
+ try {
1190
+ writeSse(res, 'response.failed', { type: 'response.failed', error: message || 'upstream stream failed' });
1191
+ writeSse(res, 'done', '[DONE]');
1192
+ res.end();
1193
+ } catch (_) {}
1194
+ }
1195
+
1196
+ function beginChatStreamResponsesSse(state) {
1197
+ if (!state || state.started) return;
1198
+ state.started = true;
1199
+ const res = state.res;
1200
+ if (!res.headersSent) {
1201
+ res.writeHead(200, {
1202
+ 'Content-Type': 'text/event-stream; charset=utf-8',
1203
+ 'Cache-Control': 'no-cache',
1204
+ 'Connection': 'keep-alive',
1205
+ 'X-Accel-Buffering': 'no'
1206
+ });
1207
+ }
1208
+ startChatStreamHeartbeat(state);
1209
+ if (typeof res.on === 'function' && !state.closeListenerAttached) {
1210
+ state.closeListenerAttached = true;
1211
+ res.on('close', () => {
1212
+ stopChatStreamHeartbeat(state);
1213
+ if (!state.finished && state.upstreamReq) {
1214
+ try { state.upstreamReq.destroy(new Error('client aborted')); } catch (_) {}
1215
+ }
1216
+ });
1217
+ }
1218
+ writeSse(res, 'response.created', {
1219
+ type: 'response.created',
1220
+ response: {
1221
+ id: state.responseId,
1222
+ model: state.model,
1223
+ created_at: state.createdAt
1224
+ }
1225
+ });
1226
+ }
1227
+
1228
+ function failChatStreamResponsesSse(state, message) {
1229
+ if (!state || state.finished) return;
1230
+ beginChatStreamResponsesSse(state);
1231
+ state.finished = true;
1232
+ stopChatStreamHeartbeat(state);
1233
+ failResponsesSseRaw(state.res, message);
1234
+ }
1235
+
1236
+ function createChatStreamResponsesSseState(res, model, options = {}) {
1237
+ let sequence = 0;
1238
+ return {
1239
+ res,
1240
+ upstreamReq: null,
1241
+ responseId: `resp_${crypto.randomBytes(10).toString('hex')}`,
1242
+ model: typeof model === 'string' ? model : '',
1243
+ createdAt: Math.floor(Date.now() / 1000),
1244
+ output: [],
1245
+ messageItem: null,
1246
+ messageText: '',
1247
+ toolCalls: [],
1248
+ toolTypesByName: options.toolTypesByName || {},
1249
+ finished: false,
1250
+ started: false,
1251
+ outputStarted: false,
1252
+ closeListenerAttached: false,
1253
+ nextSeq: () => {
1254
+ sequence += 1;
1255
+ return sequence;
1256
+ }
1257
+ };
1258
+ }
1259
+
1260
+ function streamChatCompletionsAsResponsesSse(targetUrl, options = {}) {
1261
+ const parsed = new URL(targetUrl);
1262
+ const transport = parsed.protocol === 'https:' ? https : http;
1263
+ const bodyText = options.body ? JSON.stringify(options.body) : '';
1264
+ const headers = {
1265
+ 'Accept': 'text/event-stream',
1266
+ ...(options.body ? { 'Content-Type': 'application/json' } : {}),
1267
+ ...(options.headers || {})
1268
+ };
1269
+ if (options.body) {
1270
+ headers['Content-Length'] = Buffer.byteLength(bodyText, 'utf-8');
1271
+ }
1272
+ const timeoutMs = Number.isFinite(options.timeoutMs)
1273
+ ? Math.max(1000, Number(options.timeoutMs))
1274
+ : 30000;
1275
+ const res = options.res;
1276
+ const model = typeof options.model === 'string' ? options.model : '';
1277
+ const sharedState = options.streamState || createChatStreamResponsesSseState(res, model, {
1278
+ toolTypesByName: options.toolTypesByName || {}
1279
+ });
1280
+
1281
+ return new Promise((resolve) => {
1282
+ let settled = false;
1283
+ let streamAccepted = false;
1284
+ const finish = (value) => {
1285
+ if (settled) return;
1286
+ settled = true;
1287
+ resolve(value);
1288
+ };
1289
+ const req = transport.request({
1290
+ protocol: parsed.protocol,
1291
+ hostname: parsed.hostname,
1292
+ port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
1293
+ method: options.method || 'POST',
1294
+ path: `${parsed.pathname}${parsed.search}`,
1295
+ headers,
1296
+ agent: parsed.protocol === 'https:' ? HTTPS_KEEP_ALIVE_AGENT : HTTP_KEEP_ALIVE_AGENT
1297
+ }, (upstreamRes) => {
1298
+ const status = upstreamRes.statusCode || 0;
1299
+ const chunks = [];
1300
+ const contentType = String(upstreamRes.headers && upstreamRes.headers['content-type'] || '');
1301
+ streamAccepted = status >= 200 && status < 300 && /text\/event-stream/i.test(contentType);
1302
+ if (streamAccepted) {
1303
+ req.setTimeout(0);
1304
+ }
1305
+ let streamState = null;
1306
+
1307
+ const handleAbort = (reason) => {
1308
+ if (settled) return;
1309
+ if (streamState) {
1310
+ if (streamState.outputStarted) {
1311
+ failChatStreamResponsesSse(streamState, reason);
1312
+ finish({ ok: true });
1313
+ return;
1314
+ }
1315
+ finish({ ok: false, retryTransient: true, error: reason || 'upstream stream failed' });
1316
+ return;
1317
+ }
1318
+ if (res.headersSent) {
1319
+ failResponsesSseRaw(res, reason);
1320
+ finish({ ok: true });
1321
+ return;
1322
+ }
1323
+ const bodyText = chunks.length ? Buffer.concat(chunks).toString('utf-8') : '';
1324
+ const transient = isTransientNetworkError(reason) || /aborted|stream aborted/i.test(String(reason || ''));
1325
+ finish({
1326
+ ok: false,
1327
+ ...(transient ? {} : { status }),
1328
+ ...(transient ? { retryTransient: true } : {}),
1329
+ error: reason,
1330
+ bodyText
1331
+ });
1332
+ };
1333
+ upstreamRes.on('error', (err) => handleAbort(err && err.message ? err.message : 'upstream stream failed'));
1334
+ upstreamRes.on('aborted', () => handleAbort('upstream stream aborted'));
1335
+
1336
+ if (status === 404 || status === 405) {
1337
+ upstreamRes.on('data', (chunk) => chunk && chunks.push(chunk));
1338
+ upstreamRes.on('end', () => finish({ retry: true, status, bodyText: chunks.length ? Buffer.concat(chunks).toString('utf-8') : '' }));
1339
+ return;
1340
+ }
1341
+
1342
+ if (status >= 400) {
1343
+ upstreamRes.on('data', (chunk) => chunk && chunks.push(chunk));
1344
+ upstreamRes.on('end', () => finish({ ok: false, status, bodyText: chunks.length ? Buffer.concat(chunks).toString('utf-8') : '' }));
1345
+ return;
1346
+ }
1347
+
1348
+ if (!/text\/event-stream/i.test(contentType)) {
1349
+ upstreamRes.on('data', (chunk) => chunk && chunks.push(chunk));
1350
+ upstreamRes.on('end', () => {
1351
+ const text = chunks.length ? Buffer.concat(chunks).toString('utf-8') : '';
1352
+ const parsedJson = parseJsonOrError(text);
1353
+ res.writeHead(200, {
1354
+ 'Content-Type': 'text/event-stream; charset=utf-8',
1355
+ 'Cache-Control': 'no-cache',
1356
+ 'Connection': 'keep-alive',
1357
+ 'X-Accel-Buffering': 'no'
1358
+ });
1359
+ if (parsedJson.error) {
1360
+ writeSse(res, 'response.failed', { type: 'response.failed', error: `invalid upstream response: ${parsedJson.error}` });
1361
+ writeSse(res, 'done', '[DONE]');
1362
+ res.end();
1363
+ finish({ ok: true });
1364
+ return;
1365
+ }
1366
+ sendResponsesSse(res, buildResponsesPayloadFromChatCompletion(parsedJson.value, model, {
1367
+ toolTypesByName: options.toolTypesByName || {}
1368
+ }));
1369
+ res.end();
1370
+ finish({ ok: true });
1371
+ });
1372
+ return;
1373
+ }
1374
+
1375
+ const state = sharedState;
1376
+ state.upstreamReq = req;
1377
+ if (!state.model && model) state.model = model;
1378
+ streamState = state;
1379
+ beginChatStreamResponsesSse(state);
1380
+
1381
+ let buffer = '';
1382
+ const handleEventBlock = (block) => {
1383
+ const dataLines = String(block || '')
1384
+ .split(/\r?\n/)
1385
+ .filter((line) => line.startsWith('data:'))
1386
+ .map((line) => line.slice(5).trimStart());
1387
+ if (dataLines.length === 0) return;
1388
+ const data = dataLines.join('\n').trim();
1389
+ if (!data) return;
1390
+ if (data === '[DONE]') {
1391
+ finishChatStreamResponsesSse(state);
1392
+ finish({ ok: true });
1393
+ return;
1394
+ }
1395
+ const parsedChunk = parseJsonOrError(data);
1396
+ if (!parsedChunk.error) {
1397
+ beginChatStreamResponsesSse(state);
1398
+ writeChatCompletionChunkAsResponsesSse(state, parsedChunk.value);
1399
+ }
1400
+ };
1401
+
1402
+ upstreamRes.on('data', (chunk) => {
1403
+ buffer += chunk.toString('utf-8');
1404
+ let boundary = buffer.search(/\r?\n\r?\n/);
1405
+ while (boundary >= 0) {
1406
+ const block = buffer.slice(0, boundary);
1407
+ const match = buffer.slice(boundary).match(/^\r?\n\r?\n/);
1408
+ buffer = buffer.slice(boundary + (match ? match[0].length : 2));
1409
+ handleEventBlock(block);
1410
+ boundary = buffer.search(/\r?\n\r?\n/);
1411
+ }
1412
+ });
1413
+ upstreamRes.on('end', () => {
1414
+ if (buffer.trim()) handleEventBlock(buffer);
1415
+ finishChatStreamResponsesSse(state);
1416
+ finish({ ok: true });
1417
+ });
1418
+ });
1419
+ req.setTimeout(timeoutMs, () => {
1420
+ if (streamAccepted) return;
1421
+ try { req.destroy(new Error('timeout')); } catch (_) {}
1422
+ finish({ ok: false, error: 'timeout' });
1423
+ });
1424
+ req.on('error', (err) => finish({ ok: false, error: err && err.message ? err.message : 'request failed' }));
1425
+ if (bodyText) req.write(bodyText);
1426
+ req.end();
1427
+ });
1428
+ }
1429
+
1430
+ async function streamChatCompletionsAsResponsesSseWithFallbackUrls(baseUrl, pathSuffix, options = {}) {
1431
+ const urls = buildUpstreamUrlCandidates(baseUrl, pathSuffix);
1432
+ if (urls.length === 0) {
1433
+ return { ok: false, error: 'failed to build upstream URL' };
1434
+ }
1435
+ let lastResult = null;
1436
+ const streamState = options.streamState || createChatStreamResponsesSseState(options.res, options.model, {
1437
+ toolTypesByName: options.toolTypesByName || {}
1438
+ });
1439
+ for (const url of urls) {
1440
+ const result = await retryTransientRequest(() => streamChatCompletionsAsResponsesSse(url, { ...options, streamState }));
1441
+ lastResult = result;
1442
+ if (result && result.retry) continue;
1443
+ return result;
1444
+ }
1445
+ return lastResult || { ok: false, error: 'failed to build upstream URL' };
1446
+ }
1447
+
1448
+ function canListenPort(host, port) {
1449
+ return new Promise((resolve) => {
1450
+ const tester = net.createServer();
1451
+ tester.unref();
1452
+ tester.once('error', () => resolve(false));
1453
+ tester.once('listening', () => {
1454
+ tester.close(() => resolve(true));
1455
+ });
1456
+ tester.listen(port, host);
1457
+ });
1458
+ }
1459
+
1460
+ async function findAvailablePort(host, startPort, maxAttempts = 20) {
1461
+ const start = parseInt(String(startPort), 10);
1462
+ if (!Number.isFinite(start) || start <= 0) {
1463
+ return 0;
1464
+ }
1465
+ const attempts = Number.isFinite(maxAttempts) && maxAttempts > 0 ? maxAttempts : 20;
1466
+ for (let offset = 0; offset < attempts; offset += 1) {
1467
+ const candidate = start + offset;
1468
+ if (candidate > 65535) {
1469
+ break;
1470
+ }
1471
+ // eslint-disable-next-line no-await-in-loop
1472
+ const ok = await canListenPort(host, candidate);
1473
+ if (ok) {
1474
+ return candidate;
1475
+ }
1476
+ }
1477
+ return 0;
1478
+ }
1479
+
1480
+ function resolveBuiltinProxyProviderName(rawProviderName, providers = {}, preferredProvider = '') {
1481
+ const providerMap = providers && isPlainObject(providers) ? providers : {};
1482
+ const providerNames = Object.keys(providerMap)
1483
+ .filter((name) => name && !isBuiltinManagedProvider(name));
1484
+ const requested = typeof rawProviderName === 'string' ? rawProviderName.trim() : '';
1485
+ if (requested && !isBuiltinManagedProvider(requested) && providerMap[requested]) {
1486
+ return requested;
1487
+ }
1488
+ const preferred = typeof preferredProvider === 'string' ? preferredProvider.trim() : '';
1489
+ if (preferred && !isBuiltinManagedProvider(preferred) && providerMap[preferred]) {
1490
+ return preferred;
1491
+ }
1492
+ return providerNames[0] || '';
1493
+ }
1494
+
1495
+ function normalizeBuiltinProxySettings(raw) {
1496
+ const merged = {
1497
+ ...DEFAULT_BUILTIN_PROXY_SETTINGS,
1498
+ ...(isPlainObject(raw) ? raw : {})
1499
+ };
1500
+ const host = typeof merged.host === 'string' ? merged.host.trim() : '';
1501
+ const port = parseInt(String(merged.port), 10);
1502
+ const provider = typeof merged.provider === 'string' ? merged.provider.trim() : '';
1503
+ const authSourceRaw = typeof merged.authSource === 'string' ? merged.authSource.trim().toLowerCase() : '';
1504
+ const timeoutMs = parseInt(String(merged.timeoutMs), 10);
1505
+ const authSource = authSourceRaw === 'profile' || authSourceRaw === 'none' ? authSourceRaw : 'provider';
1506
+
1507
+ return {
1508
+ enabled: merged.enabled !== false,
1509
+ host: host || DEFAULT_BUILTIN_PROXY_SETTINGS.host,
1510
+ port: Number.isFinite(port) && port > 0 && port <= 65535 ? port : DEFAULT_BUILTIN_PROXY_SETTINGS.port,
1511
+ provider,
1512
+ authSource,
1513
+ timeoutMs: Number.isFinite(timeoutMs) && timeoutMs >= 1000
1514
+ ? timeoutMs
1515
+ : DEFAULT_BUILTIN_PROXY_SETTINGS.timeoutMs
1516
+ };
1517
+ }
1518
+
1519
+ function readBuiltinProxySettings() {
1520
+ const parsed = readJsonFile(BUILTIN_PROXY_SETTINGS_FILE, null);
1521
+ return normalizeBuiltinProxySettings(parsed);
1522
+ }
1523
+
1524
+ function saveBuiltinProxySettings(payload = {}, options = {}) {
1525
+ const current = readBuiltinProxySettings();
1526
+ const merged = normalizeBuiltinProxySettings({
1527
+ ...current,
1528
+ ...(isPlainObject(payload) ? payload : {})
1529
+ });
1530
+
1531
+ if (!merged.host) {
1532
+ return { error: '代理 host 不能为空' };
1533
+ }
1534
+ if (!Number.isFinite(merged.port) || merged.port <= 0 || merged.port > 65535) {
1535
+ return { error: '代理端口无效(1-65535)' };
1536
+ }
1537
+
1538
+ const { config } = readConfigOrVirtualDefault();
1539
+ const providers = config && isPlainObject(config.model_providers) ? config.model_providers : {};
1540
+ const preferredProvider = typeof config.model_provider === 'string' ? config.model_provider.trim() : '';
1541
+ const finalProvider = resolveBuiltinProxyProviderName(merged.provider, providers, preferredProvider);
1542
+
1543
+ const normalized = {
1544
+ ...merged,
1545
+ provider: finalProvider
1546
+ };
1547
+
1548
+ if (!options.skipWrite) {
1549
+ writeJsonAtomic(BUILTIN_PROXY_SETTINGS_FILE, normalized);
1550
+ }
1551
+
1552
+ return {
1553
+ success: true,
1554
+ settings: normalized
1555
+ };
1556
+ }
1557
+
1558
+ function buildProxyListenUrl(settings) {
1559
+ const host = formatHostForUrl(settings.host || DEFAULT_BUILTIN_PROXY_SETTINGS.host);
1560
+ return `http://${host}:${settings.port}`;
1561
+ }
1562
+
1563
+ function buildBuiltinProxyProviderBaseUrl(settings) {
1564
+ return `${buildProxyListenUrl(settings).replace(/\/+$/, '')}/v1`;
1565
+ }
1566
+
1567
+ function removePersistedBuiltinProxyProviderFromConfig() {
1568
+ if (!fs.existsSync(CONFIG_FILE)) {
1569
+ return { success: true, removed: false };
1570
+ }
1571
+
1572
+ let config;
1573
+ try {
1574
+ config = readConfig();
1575
+ } catch (e) {
1576
+ return { error: e.message || '读取 config.toml 失败' };
1577
+ }
1578
+
1579
+ if (!config.model_providers || !config.model_providers[BUILTIN_PROXY_PROVIDER_NAME]) {
1580
+ return { success: true, removed: false };
1581
+ }
1582
+
1583
+ const content = fs.readFileSync(CONFIG_FILE, 'utf-8');
1584
+ const lineEnding = content.includes('\r\n') ? '\r\n' : '\n';
1585
+ const hasBom = content.charCodeAt(0) === 0xFEFF;
1586
+ const providerConfig = config.model_providers[BUILTIN_PROXY_PROVIDER_NAME];
1587
+ const providerSegments = providerConfig && Array.isArray(providerConfig.__codexmate_legacy_segments)
1588
+ ? providerConfig.__codexmate_legacy_segments
1589
+ : null;
1590
+ const providerSegmentVariants = (() => {
1591
+ const variants = [];
1592
+ const seen = new Set();
1593
+ const pushVariant = (segments) => {
1594
+ const normalized = normalizeLegacySegments(segments);
1595
+ const key = buildLegacySegmentsKey(normalized);
1596
+ if (!key || seen.has(key)) return;
1597
+ seen.add(key);
1598
+ variants.push(normalized);
1599
+ };
1600
+ if (providerConfig && Array.isArray(providerConfig.__codexmate_legacy_segments)) {
1601
+ pushVariant(providerConfig.__codexmate_legacy_segments);
1602
+ }
1603
+ if (providerConfig && Array.isArray(providerConfig.__codexmate_legacy_segment_variants)) {
1604
+ for (const segments of providerConfig.__codexmate_legacy_segment_variants) {
1605
+ pushVariant(segments);
1606
+ }
1607
+ }
1608
+ if (providerSegments) {
1609
+ pushVariant(providerSegments);
1610
+ }
1611
+ if (variants.length === 0) {
1612
+ pushVariant(String(BUILTIN_PROXY_PROVIDER_NAME || '').split('.').filter((item) => item));
1613
+ }
1614
+ return variants;
1615
+ })();
1616
+
1617
+ let updatedContent = null;
1618
+ const combinedRanges = [];
1619
+ for (const segments of providerSegmentVariants) {
1620
+ combinedRanges.push(...findProviderSectionRanges(content, BUILTIN_PROXY_PROVIDER_NAME, segments));
1621
+ combinedRanges.push(...findProviderDescendantSectionRanges(content, segments));
1622
+ }
1623
+ if (combinedRanges.length === 0) {
1624
+ combinedRanges.push(...findProviderSectionRanges(content, BUILTIN_PROXY_PROVIDER_NAME, providerSegments));
1625
+ }
1626
+
1627
+ if (combinedRanges.length > 0) {
1628
+ const sorted = combinedRanges.sort((a, b) => b.start - a.start || b.end - a.end);
1629
+ const seen = new Set();
1630
+ let removedContent = content;
1631
+ for (const range of sorted) {
1632
+ const rangeKey = `${range.start}:${range.end}`;
1633
+ if (seen.has(rangeKey)) continue;
1634
+ seen.add(rangeKey);
1635
+ removedContent = removedContent.slice(0, range.start) + removedContent.slice(range.end);
1636
+ }
1637
+ updatedContent = removedContent.replace(/\n{3,}/g, lineEnding + lineEnding);
1638
+ }
1639
+
1640
+ if (!updatedContent) {
1641
+ const rebuilt = JSON.parse(JSON.stringify(config));
1642
+ delete rebuilt.model_providers[BUILTIN_PROXY_PROVIDER_NAME];
1643
+ const hasMarker = content.includes(CODEXMATE_MANAGED_MARKER);
1644
+ let rebuiltToml = toml.stringify(rebuilt).trimEnd();
1645
+ rebuiltToml = rebuiltToml.replace(/\n/g, lineEnding);
1646
+ if (hasMarker && !rebuiltToml.includes(CODEXMATE_MANAGED_MARKER)) {
1647
+ rebuiltToml = `${CODEXMATE_MANAGED_MARKER}${lineEnding}${rebuiltToml}`;
1648
+ }
1649
+ updatedContent = rebuiltToml + lineEnding;
1650
+ if (hasBom && updatedContent.charCodeAt(0) !== 0xFEFF) {
1651
+ updatedContent = '\uFEFF' + updatedContent;
1652
+ }
1653
+ }
1654
+
1655
+ try {
1656
+ writeConfig(updatedContent.trimEnd() + lineEnding);
1657
+ } catch (e) {
1658
+ return { error: e.message || '写入 config.toml 失败' };
1659
+ }
1660
+
1661
+ return { success: true, removed: true };
1662
+ }
1663
+
1664
+ function hasCodexConfigReadyForProxy() {
1665
+ const result = readConfigOrVirtualDefault();
1666
+ if (!result || result.isVirtual) {
1667
+ return false;
1668
+ }
1669
+ const config = result.config || {};
1670
+ if (!isPlainObject(config.model_providers)) {
1671
+ return false;
1672
+ }
1673
+ const providerNames = Object.keys(config.model_providers)
1674
+ .filter((name) => name && !isBuiltinManagedProvider(name));
1675
+ return providerNames.length > 0;
1676
+ }
1677
+
1678
+ function resolveBuiltinProxyUpstream(settings) {
1679
+ const { config } = readConfigOrVirtualDefault();
1680
+ const providers = config && isPlainObject(config.model_providers) ? config.model_providers : {};
1681
+ const currentProvider = typeof config.model_provider === 'string' ? config.model_provider.trim() : '';
1682
+ const providerName = resolveBuiltinProxyProviderName(settings.provider, providers, currentProvider);
1683
+ if (!providerName) {
1684
+ return { error: '未找到可用的上游 provider,请先添加 provider' };
1685
+ }
1686
+ if (providerName === BUILTIN_PROXY_PROVIDER_NAME) {
1687
+ return { error: `上游 provider 不能是 ${BUILTIN_PROXY_PROVIDER_NAME}` };
1688
+ }
1689
+ const provider = providers[providerName];
1690
+ if (!provider || !isPlainObject(provider)) {
1691
+ return { error: `上游 provider 不存在: ${providerName}` };
1692
+ }
1693
+
1694
+ const baseUrl = typeof provider.base_url === 'string' ? provider.base_url.trim() : '';
1695
+ if (!baseUrl || !isValidHttpUrl(baseUrl)) {
1696
+ return { error: `上游 provider base_url 无效: ${providerName}` };
1697
+ }
1698
+
1699
+ let token = '';
1700
+ if (settings.authSource === 'profile') {
1701
+ token = resolveAuthTokenFromCurrentProfile();
1702
+ } else if (settings.authSource === 'provider') {
1703
+ token = typeof provider.preferred_auth_method === 'string' ? provider.preferred_auth_method.trim() : '';
1704
+ if (!token) {
1705
+ token = resolveAuthTokenFromCurrentProfile();
1706
+ }
1707
+ }
1708
+
1709
+ let authHeader = '';
1710
+ if (token) {
1711
+ authHeader = /^bearer\s+/i.test(token) ? token : `Bearer ${token}`;
1712
+ }
1713
+
1714
+ return {
1715
+ providerName,
1716
+ baseUrl: normalizeBaseUrl(baseUrl),
1717
+ authHeader
1718
+ };
1719
+ }
1720
+
1721
+ function createBuiltinProxyServer(settings, upstream) {
1722
+ const connections = new Set();
1723
+ const timeoutMs = settings.timeoutMs;
1724
+ const server = http.createServer((req, res) => {
1725
+ let parsedIncoming;
1726
+ try {
1727
+ parsedIncoming = new URL(req.url || '/', 'http://localhost');
1728
+ } catch (e) {
1729
+ res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
1730
+ res.end(JSON.stringify({ error: 'invalid request path' }));
1731
+ return;
1732
+ }
1733
+
1734
+ const remoteAddr = req && req.socket ? req.socket.remoteAddress : '';
1735
+ const isLoopback = !remoteAddr
1736
+ || remoteAddr === '127.0.0.1'
1737
+ || remoteAddr === '::1'
1738
+ || remoteAddr === '::ffff:127.0.0.1';
1739
+ if (!isLoopback) {
1740
+ const expected = typeof process.env.CODEXMATE_HTTP_TOKEN === 'string'
1741
+ ? process.env.CODEXMATE_HTTP_TOKEN.trim()
1742
+ : '';
1743
+ if (!expected) {
1744
+ const body = JSON.stringify({ error: 'Remote access is disabled (set CODEXMATE_HTTP_TOKEN)' });
1745
+ res.writeHead(403, {
1746
+ 'Content-Type': 'application/json; charset=utf-8',
1747
+ 'Content-Length': Buffer.byteLength(body, 'utf-8')
1748
+ });
1749
+ res.end(body, 'utf-8');
1750
+ return;
1751
+ }
1752
+ const headers = req && req.headers && typeof req.headers === 'object' ? req.headers : {};
1753
+ const rawAuth = typeof headers.authorization === 'string' ? headers.authorization.trim() : '';
1754
+ const match = rawAuth ? rawAuth.match(/^bearer\s+(.+)$/i) : null;
1755
+ const actual = match && match[1]
1756
+ ? match[1].trim()
1757
+ : (rawAuth ? rawAuth : (typeof headers['x-codexmate-token'] === 'string' ? String(headers['x-codexmate-token']).trim() : ''));
1758
+ if (!actual || actual !== expected) {
1759
+ const body = JSON.stringify({ error: 'Unauthorized' });
1760
+ res.writeHead(401, {
1761
+ 'Content-Type': 'application/json; charset=utf-8',
1762
+ 'Content-Length': Buffer.byteLength(body, 'utf-8')
1763
+ });
1764
+ res.end(body, 'utf-8');
1765
+ return;
1766
+ }
1767
+ }
1768
+
1769
+ const incomingPath = parsedIncoming.pathname || '/';
1770
+ if (incomingPath === '/health' || incomingPath === '/status') {
1771
+ const body = JSON.stringify({
1772
+ ok: true,
1773
+ upstreamProvider: upstream.providerName,
1774
+ upstreamBaseUrl: upstream.baseUrl
1775
+ });
1776
+ res.writeHead(200, {
1777
+ 'Content-Type': 'application/json; charset=utf-8',
1778
+ 'Content-Length': Buffer.byteLength(body, 'utf-8')
1779
+ });
1780
+ res.end(body, 'utf-8');
1781
+ return;
1782
+ }
1783
+
1784
+ if (!(incomingPath === '/v1' || incomingPath.startsWith('/v1/'))) {
1785
+ const body = JSON.stringify({ error: 'proxy only supports /v1/* paths' });
1786
+ res.writeHead(404, {
1787
+ 'Content-Type': 'application/json; charset=utf-8',
1788
+ 'Content-Length': Buffer.byteLength(body, 'utf-8')
1789
+ });
1790
+ res.end(body, 'utf-8');
1791
+ return;
1792
+ }
1793
+
1794
+ // Responses shim:
1795
+ // - Codex CLI 默认走 /v1/responses(含 SSE)
1796
+ // - SSE/streaming 任务优先走 chat/completions fallback,避免卡在会接收但不产出 Responses 的兼容网关
1797
+ // - 非流式请求仍优先尝试 /v1/responses(stream=false),失败再转换到 chat/completions 并回包为 responses。
1798
+ if ((incomingPath === '/v1/responses' || incomingPath === '/v1/responses/') && (req.method || 'GET').toUpperCase() === 'POST') {
1799
+ void (async () => {
1800
+ const { body, error } = await readRequestBody(req, 10 * 1024 * 1024);
1801
+ if (error) {
1802
+ res.writeHead(413, { 'Content-Type': 'application/json; charset=utf-8' });
1803
+ res.end(JSON.stringify({ error }));
1804
+ return;
1805
+ }
1806
+ const parsed = parseJsonOrError(body);
1807
+ if (parsed.error) {
1808
+ res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
1809
+ res.end(JSON.stringify({ error: `invalid json: ${parsed.error}` }));
1810
+ return;
1811
+ }
1812
+
1813
+ const payload = parsed.value && typeof parsed.value === 'object' ? parsed.value : {};
1814
+ const wantsStream = payload.stream === true;
1815
+
1816
+ const commonHeaders = {
1817
+ ...(upstream.authHeader ? { 'Authorization': upstream.authHeader } : {}),
1818
+ 'X-Codexmate-Proxy': '1'
1819
+ };
1820
+
1821
+ const model = typeof payload.model === 'string' ? payload.model : '';
1822
+ const chatBody = buildChatCompletionsBodyFromResponsesPayload(payload);
1823
+ const toolTypesByName = collectResponsesToolTypesByName(payload.tools);
1824
+
1825
+ if (wantsStream) {
1826
+ const streamingChatBody = { ...chatBody, stream: true };
1827
+ const streamed = await streamChatCompletionsAsResponsesSseWithFallbackUrls(upstream.baseUrl, 'chat/completions', {
1828
+ method: 'POST',
1829
+ headers: commonHeaders,
1830
+ timeoutMs,
1831
+ body: streamingChatBody,
1832
+ res,
1833
+ model,
1834
+ toolTypesByName
1835
+ });
1836
+ if (!streamed.ok) {
1837
+ if (!res.headersSent) {
1838
+ res.writeHead(streamed.status && streamed.status >= 400 ? streamed.status : 502, { 'Content-Type': 'application/json; charset=utf-8' });
1839
+ res.end(streamed.bodyText || JSON.stringify({ error: streamed.error || 'proxy request failed' }));
1840
+ } else if (!res.writableEnded) {
1841
+ writeSse(res, 'response.failed', { type: 'response.failed', error: streamed.error || streamed.bodyText || 'proxy request failed' });
1842
+ writeSse(res, 'done', '[DONE]');
1843
+ res.end();
1844
+ }
1845
+ }
1846
+ return;
1847
+ }
1848
+
1849
+ const upstreamResponses = await proxyRequestJsonWithFallbackUrls(upstream.baseUrl, 'responses', {
1850
+ method: 'POST',
1851
+ headers: commonHeaders,
1852
+ timeoutMs,
1853
+ body: { ...payload, stream: false }
1854
+ });
1855
+
1856
+ // 优先走上游 /responses(如果支持)。若上游报错且不是“端点不支持”,则直接透传错误。
1857
+ if (upstreamResponses.ok && upstreamResponses.status >= 200 && upstreamResponses.status < 300) {
1858
+ const json = parseJsonOrError(upstreamResponses.bodyText);
1859
+ if (json.error) {
1860
+ res.writeHead(502, { 'Content-Type': 'application/json; charset=utf-8' });
1861
+ res.end(JSON.stringify({ error: `Upstream JSON parse failed: ${json.error}` }));
1862
+ return;
1863
+ }
1864
+ const responsesPayload = ensureResponseMetadata(json.value);
1865
+ if (wantsStream) {
1866
+ res.writeHead(200, {
1867
+ 'Content-Type': 'text/event-stream; charset=utf-8',
1868
+ 'Cache-Control': 'no-cache',
1869
+ 'Connection': 'keep-alive',
1870
+ 'X-Accel-Buffering': 'no'
1871
+ });
1872
+ sendResponsesSse(res, responsesPayload);
1873
+ res.end();
1874
+ return;
1875
+ }
1876
+ res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
1877
+ res.end(JSON.stringify(responsesPayload));
1878
+ return;
1879
+ }
1880
+
1881
+ if (upstreamResponses.ok && upstreamResponses.status >= 400) {
1882
+ if (!shouldFallbackFromUpstreamResponses(upstreamResponses.status, upstreamResponses.bodyText)) {
1883
+ res.writeHead(upstreamResponses.status, { 'Content-Type': 'application/json; charset=utf-8' });
1884
+ res.end(upstreamResponses.bodyText || JSON.stringify({ error: 'Upstream error' }));
1885
+ return;
1886
+ }
1887
+ // fallthrough to chat/completions conversion
1888
+ }
1889
+
1890
+ if (!upstreamResponses.ok) {
1891
+ if (!shouldFallbackFromUpstreamResponsesFailure(upstreamResponses.error)) {
1892
+ res.writeHead(502, { 'Content-Type': 'application/json; charset=utf-8' });
1893
+ res.end(JSON.stringify({ error: upstreamResponses.error || 'Upstream request failed' }));
1894
+ return;
1895
+ }
1896
+ // Some OpenAI-compatible gateways accept /responses but never complete it.
1897
+ // Treat that as an unsupported Responses endpoint and try the chat fallback.
1898
+ }
1899
+
1900
+ const upstreamChat = await proxyRequestJsonWithFallbackUrls(upstream.baseUrl, 'chat/completions', {
1901
+ method: 'POST',
1902
+ headers: commonHeaders,
1903
+ timeoutMs,
1904
+ body: chatBody
1905
+ });
1906
+ if (!upstreamChat.ok) {
1907
+ res.writeHead(502, { 'Content-Type': 'application/json; charset=utf-8' });
1908
+ res.end(JSON.stringify({ error: upstreamChat.error || 'proxy request failed' }));
1909
+ return;
1910
+ }
1911
+
1912
+ if (upstreamChat.status >= 400) {
1913
+ res.writeHead(upstreamChat.status, { 'Content-Type': 'application/json; charset=utf-8' });
1914
+ res.end(upstreamChat.bodyText || JSON.stringify({ error: 'Upstream error' }));
1915
+ return;
1916
+ }
1917
+
1918
+ const chatJson = parseJsonOrError(upstreamChat.bodyText);
1919
+ if (chatJson.error) {
1920
+ res.writeHead(502, { 'Content-Type': 'application/json; charset=utf-8' });
1921
+ res.end(JSON.stringify({ error: `invalid upstream response: ${chatJson.error}` }));
1922
+ return;
1923
+ }
1924
+
1925
+ const responsesPayload = buildResponsesPayloadFromChatCompletion(chatJson.value, model, { toolTypesByName });
1926
+
1927
+ if (wantsStream) {
1928
+ res.writeHead(200, {
1929
+ 'Content-Type': 'text/event-stream; charset=utf-8',
1930
+ 'Cache-Control': 'no-cache',
1931
+ 'Connection': 'keep-alive',
1932
+ 'X-Accel-Buffering': 'no'
1933
+ });
1934
+ sendResponsesSse(res, responsesPayload);
1935
+ res.end();
1936
+ return;
1937
+ }
1938
+ res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
1939
+ res.end(JSON.stringify(responsesPayload));
1940
+ })();
1941
+ return;
1942
+ }
1943
+
1944
+ const suffix = incomingPath === '/v1'
1945
+ ? ''
1946
+ : incomingPath.replace(/^\/v1\/?/, '');
1947
+ const targetBase = joinApiUrl(upstream.baseUrl, suffix);
1948
+ if (!targetBase) {
1949
+ const body = JSON.stringify({ error: 'failed to build upstream URL' });
1950
+ res.writeHead(500, {
1951
+ 'Content-Type': 'application/json; charset=utf-8',
1952
+ 'Content-Length': Buffer.byteLength(body, 'utf-8')
1953
+ });
1954
+ res.end(body, 'utf-8');
1955
+ return;
1956
+ }
1957
+
1958
+ let targetUrl;
1959
+ try {
1960
+ targetUrl = new URL(targetBase);
1961
+ targetUrl.search = parsedIncoming.search || '';
1962
+ } catch (e) {
1963
+ const body = JSON.stringify({ error: `invalid upstream URL: ${e.message}` });
1964
+ res.writeHead(500, {
1965
+ 'Content-Type': 'application/json; charset=utf-8',
1966
+ 'Content-Length': Buffer.byteLength(body, 'utf-8')
1967
+ });
1968
+ res.end(body, 'utf-8');
1969
+ return;
1970
+ }
1971
+
1972
+ const requestHeaders = { ...req.headers };
1973
+ delete requestHeaders.host;
1974
+ delete requestHeaders.connection;
1975
+ delete requestHeaders['content-length'];
1976
+ if (upstream.authHeader) {
1977
+ requestHeaders.authorization = upstream.authHeader;
1978
+ }
1979
+ requestHeaders['x-codexmate-proxy'] = '1';
1980
+ if (!requestHeaders['x-forwarded-for'] && req.socket && req.socket.remoteAddress) {
1981
+ requestHeaders['x-forwarded-for'] = req.socket.remoteAddress;
1982
+ }
1983
+
1984
+ const transport = targetUrl.protocol === 'https:' ? https : http;
1985
+ const upstreamReq = transport.request({
1986
+ protocol: targetUrl.protocol,
1987
+ hostname: targetUrl.hostname,
1988
+ port: targetUrl.port || (targetUrl.protocol === 'https:' ? 443 : 80),
1989
+ method: req.method || 'GET',
1990
+ path: `${targetUrl.pathname}${targetUrl.search}`,
1991
+ headers: requestHeaders,
1992
+ agent: targetUrl.protocol === 'https:' ? HTTPS_KEEP_ALIVE_AGENT : HTTP_KEEP_ALIVE_AGENT
1993
+ }, (upstreamRes) => {
1994
+ const responseHeaders = { ...upstreamRes.headers };
1995
+ delete responseHeaders.connection;
1996
+ res.writeHead(upstreamRes.statusCode || 502, responseHeaders);
1997
+ upstreamRes.pipe(res);
1998
+ });
1999
+
2000
+ upstreamReq.setTimeout(timeoutMs, () => {
2001
+ upstreamReq.destroy(new Error(`upstream timeout (${timeoutMs}ms)`));
2002
+ });
2003
+
2004
+ upstreamReq.on('error', (err) => {
2005
+ if (res.headersSent) {
2006
+ try { res.destroy(err); } catch (_) {}
2007
+ return;
2008
+ }
2009
+ const body = JSON.stringify({ error: `proxy request failed: ${err.message}` });
2010
+ res.writeHead(502, {
2011
+ 'Content-Type': 'application/json; charset=utf-8',
2012
+ 'Content-Length': Buffer.byteLength(body, 'utf-8')
2013
+ });
2014
+ res.end(body, 'utf-8');
2015
+ });
2016
+
2017
+ req.pipe(upstreamReq);
2018
+ });
2019
+
2020
+ server.on('connection', (socket) => {
2021
+ connections.add(socket);
2022
+ socket.on('close', () => connections.delete(socket));
2023
+ });
2024
+
2025
+ return new Promise((resolve, reject) => {
2026
+ server.once('error', reject);
2027
+ server.listen(settings.port, settings.host, () => {
2028
+ server.removeListener('error', reject);
2029
+ resolve({
2030
+ server,
2031
+ connections,
2032
+ settings,
2033
+ upstream,
2034
+ startedAt: toIsoTime(Date.now()),
2035
+ listenUrl: buildProxyListenUrl(settings)
2036
+ });
2037
+ });
2038
+ });
2039
+ }
2040
+
2041
+ async function startBuiltinProxyRuntime(payload = {}) {
2042
+ if (runtime) {
2043
+ return {
2044
+ error: '内建代理已在运行',
2045
+ runtime: {
2046
+ listenUrl: runtime.listenUrl,
2047
+ upstreamProvider: runtime.upstream.providerName
2048
+ }
2049
+ };
2050
+ }
2051
+
2052
+ const saveResult = saveBuiltinProxySettings(payload);
2053
+ if (saveResult.error) {
2054
+ return { error: saveResult.error };
2055
+ }
2056
+ const settings = saveResult.settings;
2057
+ const upstream = resolveBuiltinProxyUpstream(settings);
2058
+ if (upstream.error) {
2059
+ return { error: upstream.error };
2060
+ }
2061
+
2062
+ try {
2063
+ runtime = await createBuiltinProxyServer(settings, upstream);
2064
+ return {
2065
+ success: true,
2066
+ running: true,
2067
+ listenUrl: runtime.listenUrl,
2068
+ upstreamProvider: upstream.providerName,
2069
+ settings
2070
+ };
2071
+ } catch (e) {
2072
+ return { error: `启动内建代理失败: ${e.message}` };
2073
+ }
2074
+ }
2075
+
2076
+ async function stopBuiltinProxyRuntime() {
2077
+ if (!runtime) {
2078
+ return { success: true, running: false };
2079
+ }
2080
+ const currentRuntime = runtime;
2081
+ runtime = null;
2082
+
2083
+ await new Promise((resolve) => {
2084
+ let settled = false;
2085
+ const finish = () => {
2086
+ if (settled) return;
2087
+ settled = true;
2088
+ resolve();
2089
+ };
2090
+
2091
+ currentRuntime.server.close(() => finish());
2092
+ setTimeout(() => finish(), 1000);
2093
+ });
2094
+
2095
+ for (const socket of currentRuntime.connections) {
2096
+ try { socket.destroy(); } catch (_) {}
2097
+ }
2098
+ currentRuntime.connections.clear();
2099
+
2100
+ return {
2101
+ success: true,
2102
+ running: false
2103
+ };
2104
+ }
2105
+
2106
+ function getBuiltinProxyStatus() {
2107
+ const settings = readBuiltinProxySettings();
2108
+ return {
2109
+ running: !!runtime,
2110
+ settings,
2111
+ runtime: runtime
2112
+ ? {
2113
+ provider: BUILTIN_PROXY_PROVIDER_NAME,
2114
+ startedAt: runtime.startedAt,
2115
+ listenUrl: runtime.listenUrl,
2116
+ upstreamProvider: runtime.upstream.providerName,
2117
+ upstreamBaseUrl: runtime.upstream.baseUrl
2118
+ }
2119
+ : null
2120
+ };
2121
+ }
2122
+
2123
+ return {
2124
+ canListenPort,
2125
+ findAvailablePort,
2126
+ normalizeBuiltinProxySettings,
2127
+ readBuiltinProxySettings,
2128
+ resolveBuiltinProxyProviderName,
2129
+ saveBuiltinProxySettings,
2130
+ buildProxyListenUrl,
2131
+ buildBuiltinProxyProviderBaseUrl,
2132
+ removePersistedBuiltinProxyProviderFromConfig,
2133
+ hasCodexConfigReadyForProxy,
2134
+ resolveBuiltinProxyUpstream,
2135
+ createBuiltinProxyServer,
2136
+ startBuiltinProxyRuntime,
2137
+ stopBuiltinProxyRuntime,
2138
+ getBuiltinProxyStatus
2139
+ };
2140
+ }
2141
+
2142
+ module.exports = {
2143
+ createBuiltinProxyRuntimeController
2144
+ };