codexmate 0.0.32 → 0.0.34

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 (148) hide show
  1. package/README.md +147 -363
  2. package/README.zh.md +147 -371
  3. package/cli/agents-files.js +230 -224
  4. package/cli/archive-helpers.js +453 -446
  5. package/cli/auth-profiles.js +375 -375
  6. package/cli/builtin-proxy.js +1725 -1725
  7. package/cli/claude-proxy.js +1022 -1022
  8. package/cli/config-bootstrap.js +402 -402
  9. package/cli/config-health.js +454 -454
  10. package/cli/doctor-core.js +903 -903
  11. package/cli/import-skills-url.js +356 -356
  12. package/cli/local-bridge.js +556 -324
  13. package/cli/openai-bridge.js +1653 -1653
  14. package/cli/openclaw-config.js +629 -629
  15. package/cli/session-convert-args.js +69 -69
  16. package/cli/session-convert-io.js +82 -82
  17. package/cli/session-convert.js +150 -150
  18. package/cli/session-usage.concurrent.js +28 -28
  19. package/cli/session-usage.js +118 -118
  20. package/cli/session-usage.models.js +176 -176
  21. package/cli/skills.js +1141 -1141
  22. package/cli/update.js +171 -0
  23. package/cli/zip-commands.js +510 -510
  24. package/cli.js +16079 -15829
  25. package/lib/automation.js +404 -404
  26. package/lib/cli-file-utils.js +151 -151
  27. package/lib/cli-models-utils.js +440 -440
  28. package/lib/cli-network-utils.js +190 -190
  29. package/lib/cli-path-utils.js +85 -85
  30. package/lib/cli-session-utils.js +121 -121
  31. package/lib/cli-sessions.js +427 -426
  32. package/lib/cli-utils.js +155 -155
  33. package/lib/cli-webhook.js +154 -126
  34. package/lib/download-artifacts.js +92 -92
  35. package/lib/mcp-stdio.js +453 -453
  36. package/lib/task-orchestrator.js +869 -869
  37. package/lib/text-diff.js +303 -303
  38. package/lib/win-tray.js +119 -0
  39. package/lib/workflow-engine.js +340 -340
  40. package/package.json +76 -76
  41. package/plugins/README.md +20 -20
  42. package/plugins/README.zh-CN.md +20 -20
  43. package/plugins/prompt-templates/comment-polish/index.mjs +25 -25
  44. package/plugins/prompt-templates/computed.mjs +253 -253
  45. package/plugins/prompt-templates/index.mjs +8 -8
  46. package/plugins/prompt-templates/manifest.mjs +15 -15
  47. package/plugins/prompt-templates/methods.mjs +553 -553
  48. package/plugins/prompt-templates/overview.mjs +91 -91
  49. package/plugins/prompt-templates/ownership.mjs +19 -19
  50. package/plugins/prompt-templates/rule-ack/index.mjs +21 -21
  51. package/plugins/prompt-templates/storage.mjs +64 -64
  52. package/plugins/registry.mjs +16 -16
  53. package/web-ui/app.js +654 -647
  54. package/web-ui/index.html +37 -36
  55. package/web-ui/logic.agents-diff.mjs +386 -386
  56. package/web-ui/logic.claude.mjs +172 -168
  57. package/web-ui/logic.codex.mjs +69 -69
  58. package/web-ui/logic.mjs +5 -5
  59. package/web-ui/logic.runtime.mjs +128 -128
  60. package/web-ui/logic.session-convert.mjs +70 -70
  61. package/web-ui/logic.sessions.mjs +781 -781
  62. package/web-ui/modules/api.mjs +90 -90
  63. package/web-ui/modules/app.computed.dashboard.mjs +248 -248
  64. package/web-ui/modules/app.computed.index.mjs +17 -17
  65. package/web-ui/modules/app.computed.main-tabs.mjs +205 -205
  66. package/web-ui/modules/app.computed.session.mjs +735 -693
  67. package/web-ui/modules/app.constants.mjs +15 -15
  68. package/web-ui/modules/app.methods.agents.mjs +651 -651
  69. package/web-ui/modules/app.methods.claude-config.mjs +306 -200
  70. package/web-ui/modules/app.methods.codex-config.mjs +869 -861
  71. package/web-ui/modules/app.methods.index.mjs +94 -94
  72. package/web-ui/modules/app.methods.install.mjs +205 -205
  73. package/web-ui/modules/app.methods.navigation.mjs +788 -774
  74. package/web-ui/modules/app.methods.openclaw-core.mjs +814 -814
  75. package/web-ui/modules/app.methods.openclaw-editing.mjs +372 -372
  76. package/web-ui/modules/app.methods.openclaw-persist.mjs +369 -369
  77. package/web-ui/modules/app.methods.providers.mjs +575 -529
  78. package/web-ui/modules/app.methods.runtime.mjs +345 -345
  79. package/web-ui/modules/app.methods.session-actions.mjs +591 -591
  80. package/web-ui/modules/app.methods.session-browser.mjs +1011 -1012
  81. package/web-ui/modules/app.methods.session-timeline.mjs +479 -479
  82. package/web-ui/modules/app.methods.session-trash.mjs +438 -438
  83. package/web-ui/modules/app.methods.startup-claude.mjs +547 -537
  84. package/web-ui/modules/app.methods.task-orchestration.mjs +556 -556
  85. package/web-ui/modules/app.methods.webhook.mjs +87 -79
  86. package/web-ui/modules/config-mode.computed.mjs +124 -124
  87. package/web-ui/modules/config-template-confirm-pref.mjs +33 -33
  88. package/web-ui/modules/i18n.dict.mjs +3195 -3177
  89. package/web-ui/modules/i18n.mjs +62 -62
  90. package/web-ui/modules/plugins.computed.mjs +3 -3
  91. package/web-ui/modules/plugins.methods.mjs +3 -3
  92. package/web-ui/modules/plugins.storage.mjs +11 -11
  93. package/web-ui/modules/provider-url-display.mjs +17 -17
  94. package/web-ui/modules/sessions-filters-url.mjs +85 -85
  95. package/web-ui/modules/skills.computed.mjs +107 -107
  96. package/web-ui/modules/skills.methods.mjs +482 -482
  97. package/web-ui/partials/index/layout-footer.html +13 -13
  98. package/web-ui/partials/index/layout-header.html +499 -503
  99. package/web-ui/partials/index/modal-config-template-agents.html +185 -185
  100. package/web-ui/partials/index/modal-confirm-toast.html +32 -32
  101. package/web-ui/partials/index/modal-health-check.html +45 -45
  102. package/web-ui/partials/index/modal-openclaw-config.html +280 -280
  103. package/web-ui/partials/index/modal-skills.html +200 -200
  104. package/web-ui/partials/index/modal-webhook.html +42 -0
  105. package/web-ui/partials/index/modals-basic.html +223 -162
  106. package/web-ui/partials/index/panel-config-claude.html +155 -136
  107. package/web-ui/partials/index/panel-config-codex.html +176 -196
  108. package/web-ui/partials/index/panel-config-codex.html.bak +337 -0
  109. package/web-ui/partials/index/panel-config-openclaw.html +83 -83
  110. package/web-ui/partials/index/panel-dashboard.html +186 -219
  111. package/web-ui/partials/index/panel-docs.html +114 -114
  112. package/web-ui/partials/index/panel-market.html +177 -177
  113. package/web-ui/partials/index/panel-orchestration.html +391 -391
  114. package/web-ui/partials/index/panel-plugins.html +253 -253
  115. package/web-ui/partials/index/panel-sessions.html +319 -313
  116. package/web-ui/partials/index/panel-settings.html +158 -190
  117. package/web-ui/partials/index/panel-trash.html +82 -82
  118. package/web-ui/partials/index/panel-usage.html +137 -137
  119. package/web-ui/res/json5.min.js +1 -1
  120. package/web-ui/res/vue.global.prod.js +13 -13
  121. package/web-ui/session-helpers.mjs +591 -591
  122. package/web-ui/source-bundle.cjs +233 -233
  123. package/web-ui/styles/base-theme.css +281 -281
  124. package/web-ui/styles/bridge-pool.css +266 -197
  125. package/web-ui/styles/controls-forms.css +433 -433
  126. package/web-ui/styles/dashboard.css +406 -406
  127. package/web-ui/styles/docs-panel.css +245 -245
  128. package/web-ui/styles/feedback.css +108 -108
  129. package/web-ui/styles/health-check-dialog.css +144 -144
  130. package/web-ui/styles/layout-shell.css +628 -638
  131. package/web-ui/styles/modals-core.css +499 -466
  132. package/web-ui/styles/navigation-panels.css +391 -391
  133. package/web-ui/styles/openclaw-structured.css +266 -266
  134. package/web-ui/styles/plugins-panel.css +564 -564
  135. package/web-ui/styles/responsive.css +392 -392
  136. package/web-ui/styles/sessions-list.css +683 -647
  137. package/web-ui/styles/sessions-preview.css +407 -407
  138. package/web-ui/styles/sessions-toolbar-trash.css +518 -518
  139. package/web-ui/styles/sessions-usage.css +588 -588
  140. package/web-ui/styles/settings-panel.css +415 -349
  141. package/web-ui/styles/skills-list.css +305 -305
  142. package/web-ui/styles/skills-market.css +429 -429
  143. package/web-ui/styles/task-orchestration.css +822 -822
  144. package/web-ui/styles/titles-cards.css +472 -472
  145. package/web-ui/styles/trash-panel.css +90 -90
  146. package/web-ui/styles/webhook.css +115 -81
  147. package/web-ui/styles.css +24 -24
  148. package/web-ui.html +17 -17
@@ -1,1022 +1,1022 @@
1
- const http = require('http');
2
- const https = require('https');
3
- const crypto = require('crypto');
4
- const { readJsonFile, writeJsonAtomic } = require('../lib/cli-file-utils');
5
- const { isValidHttpUrl, normalizeBaseUrl, joinApiUrl } = require('../lib/cli-utils');
6
- const {
7
- extractModelNames,
8
- extractModelResponseText,
9
- normalizeWireApi
10
- } = require('../lib/cli-models-utils');
11
- const { toIsoTime } = require('../lib/cli-session-utils');
12
-
13
- function isPlainObject(value) {
14
- return !!value && typeof value === 'object' && !Array.isArray(value);
15
- }
16
-
17
- function readNonNegativeInteger(value) {
18
- if (typeof value === 'number' && Number.isFinite(value) && value >= 0) {
19
- return Math.floor(value);
20
- }
21
- if (typeof value === 'string' && value.trim()) {
22
- const parsed = Number.parseInt(value.trim(), 10);
23
- return Number.isFinite(parsed) && parsed >= 0 ? parsed : 0;
24
- }
25
- return 0;
26
- }
27
-
28
- function formatHostForUrl(host) {
29
- const value = typeof host === 'string' ? host.trim() : '';
30
- if (!value) return '';
31
- if (value.startsWith('[') && value.endsWith(']')) {
32
- return value;
33
- }
34
- if (value.includes(':')) {
35
- return `[${value}]`;
36
- }
37
- return value;
38
- }
39
-
40
- function normalizeAnthropicContentBlocks(content) {
41
- if (typeof content === 'string') {
42
- return content.trim() ? [{ type: 'text', text: content }] : [];
43
- }
44
- if (Array.isArray(content)) {
45
- return content.flatMap((item) => normalizeAnthropicContentBlocks(item));
46
- }
47
- if (content && typeof content === 'object') {
48
- if (typeof content.type === 'string') {
49
- return [content];
50
- }
51
- if (typeof content.text === 'string') {
52
- return [{ type: 'text', text: content.text }];
53
- }
54
- }
55
- return [];
56
- }
57
-
58
- function collectAnthropicTextContent(content) {
59
- const pieces = [];
60
- for (const block of normalizeAnthropicContentBlocks(content)) {
61
- if (block && block.type === 'text' && typeof block.text === 'string' && block.text.trim()) {
62
- pieces.push(block.text.trim());
63
- }
64
- }
65
- return pieces.join('\n\n').trim();
66
- }
67
-
68
- function safeJsonStringify(value) {
69
- try {
70
- return JSON.stringify(value);
71
- } catch (e) {
72
- return JSON.stringify(String(value));
73
- }
74
- }
75
-
76
- function stringifyAnthropicToolResultContent(content) {
77
- if (typeof content === 'string') {
78
- return content;
79
- }
80
- const text = collectAnthropicTextContent(content);
81
- if (text) {
82
- return text;
83
- }
84
- return safeJsonStringify(content);
85
- }
86
-
87
- function appendAnthropicMessageToResponsesInput(target, message) {
88
- if (!message || typeof message !== 'object') return;
89
- const roleRaw = typeof message.role === 'string' ? message.role.trim().toLowerCase() : '';
90
- const role = roleRaw === 'assistant' ? 'assistant' : 'user';
91
- const textType = role === 'assistant' ? 'output_text' : 'input_text';
92
- let buffered = [];
93
-
94
- const flushBuffered = () => {
95
- if (!buffered.length) return;
96
- target.push({ role, content: buffered });
97
- buffered = [];
98
- };
99
-
100
- for (const block of normalizeAnthropicContentBlocks(message.content)) {
101
- if (!block || typeof block !== 'object') continue;
102
- if (block.type === 'text' && typeof block.text === 'string' && block.text) {
103
- buffered.push({ type: textType, text: block.text });
104
- continue;
105
- }
106
- if (block.type === 'tool_use' && typeof block.name === 'string' && block.name.trim()) {
107
- flushBuffered();
108
- target.push({
109
- type: 'function_call',
110
- call_id: typeof block.id === 'string' && block.id.trim()
111
- ? block.id.trim()
112
- : `call_${crypto.randomBytes(8).toString('hex')}`,
113
- name: block.name.trim(),
114
- arguments: safeJsonStringify(block.input && typeof block.input === 'object' ? block.input : {})
115
- });
116
- continue;
117
- }
118
- if (block.type === 'tool_result' && typeof block.tool_use_id === 'string' && block.tool_use_id.trim()) {
119
- flushBuffered();
120
- target.push({
121
- type: 'function_call_output',
122
- call_id: block.tool_use_id.trim(),
123
- output: stringifyAnthropicToolResultContent(block.content)
124
- });
125
- continue;
126
- }
127
- buffered.push({
128
- type: textType,
129
- text: `[unsupported anthropic block: ${typeof block.type === 'string' ? block.type : 'unknown'}]`
130
- });
131
- }
132
-
133
- flushBuffered();
134
- }
135
-
136
- function mapAnthropicToolChoiceToResponses(toolChoice) {
137
- if (!toolChoice) return undefined;
138
- if (typeof toolChoice === 'string') {
139
- if (toolChoice === 'auto') return 'auto';
140
- if (toolChoice === 'any') return 'required';
141
- return undefined;
142
- }
143
- if (!toolChoice || typeof toolChoice !== 'object') return undefined;
144
- const type = typeof toolChoice.type === 'string' ? toolChoice.type.trim().toLowerCase() : '';
145
- if (type === 'auto') return 'auto';
146
- if (type === 'any') return 'required';
147
- if (type === 'tool' && typeof toolChoice.name === 'string' && toolChoice.name.trim()) {
148
- return {
149
- type: 'function',
150
- name: toolChoice.name.trim()
151
- };
152
- }
153
- return undefined;
154
- }
155
-
156
- function buildBuiltinClaudeResponsesRequest(payload = {}) {
157
- const model = typeof payload.model === 'string' ? payload.model.trim() : '';
158
- if (!model) {
159
- throw new Error('Anthropic messages 请求缺少 model');
160
- }
161
- const messages = Array.isArray(payload.messages) ? payload.messages : [];
162
- if (!messages.length) {
163
- throw new Error('Anthropic messages 请求缺少 messages');
164
- }
165
-
166
- const maxTokens = parseInt(String(payload.max_tokens), 10);
167
- const requestBody = {
168
- model,
169
- input: [],
170
- max_output_tokens: Number.isFinite(maxTokens) && maxTokens > 0 ? maxTokens : 1024
171
- };
172
-
173
- const instructions = collectAnthropicTextContent(payload.system);
174
- if (instructions) {
175
- requestBody.instructions = instructions;
176
- }
177
-
178
- for (const message of messages) {
179
- appendAnthropicMessageToResponsesInput(requestBody.input, message);
180
- }
181
-
182
- if (Number.isFinite(payload.temperature)) {
183
- requestBody.temperature = Number(payload.temperature);
184
- }
185
- if (Number.isFinite(payload.top_p)) {
186
- requestBody.top_p = Number(payload.top_p);
187
- }
188
- if (Array.isArray(payload.stop_sequences) && payload.stop_sequences.length) {
189
- requestBody.stop = payload.stop_sequences.filter((item) => typeof item === 'string' && item.trim());
190
- }
191
- if (isPlainObject(payload.metadata)) {
192
- requestBody.metadata = payload.metadata;
193
- }
194
- if (Array.isArray(payload.tools) && payload.tools.length) {
195
- requestBody.tools = payload.tools
196
- .map((tool) => {
197
- if (!tool || typeof tool !== 'object') return null;
198
- const name = typeof tool.name === 'string' ? tool.name.trim() : '';
199
- if (!name) return null;
200
- return {
201
- type: 'function',
202
- name,
203
- description: typeof tool.description === 'string' ? tool.description : '',
204
- parameters: isPlainObject(tool.input_schema) ? tool.input_schema : { type: 'object', properties: {} }
205
- };
206
- })
207
- .filter(Boolean);
208
- if (!requestBody.tools.length) {
209
- delete requestBody.tools;
210
- }
211
- }
212
-
213
- const toolChoice = mapAnthropicToolChoiceToResponses(payload.tool_choice);
214
- if (toolChoice !== undefined) {
215
- requestBody.tool_choice = toolChoice;
216
- }
217
-
218
- return requestBody;
219
- }
220
-
221
- function parseJsonObjectLoose(value) {
222
- if (value && typeof value === 'object' && !Array.isArray(value)) {
223
- return value;
224
- }
225
- if (typeof value !== 'string' || !value.trim()) {
226
- return {};
227
- }
228
- try {
229
- const parsed = JSON.parse(value);
230
- return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
231
- } catch (e) {
232
- return {};
233
- }
234
- }
235
-
236
- function readResponsesUsageValue(value) {
237
- const parsed = readNonNegativeInteger(value);
238
- return parsed > 0 ? parsed : 0;
239
- }
240
-
241
- function buildAnthropicUsageFromResponses(payload) {
242
- const usage = payload && payload.usage && typeof payload.usage === 'object' ? payload.usage : {};
243
- return {
244
- input_tokens: readResponsesUsageValue(usage.input_tokens),
245
- output_tokens: readResponsesUsageValue(usage.output_tokens)
246
- };
247
- }
248
-
249
- function collectAnthropicContentFromResponsesOutput(payload) {
250
- const content = [];
251
- const output = Array.isArray(payload && payload.output) ? payload.output : [];
252
- for (const item of output) {
253
- if (!item || typeof item !== 'object') continue;
254
- if (item.type === 'function_call') {
255
- content.push({
256
- type: 'tool_use',
257
- id: typeof item.call_id === 'string' && item.call_id.trim()
258
- ? item.call_id.trim()
259
- : (typeof item.id === 'string' && item.id.trim()
260
- ? item.id.trim()
261
- : `toolu_${crypto.randomBytes(8).toString('hex')}`),
262
- name: typeof item.name === 'string' ? item.name : '',
263
- input: parseJsonObjectLoose(item.arguments)
264
- });
265
- continue;
266
- }
267
- if (item.type === 'message' && Array.isArray(item.content)) {
268
- for (const block of item.content) {
269
- if (!block || typeof block !== 'object') continue;
270
- if ((block.type === 'output_text' || block.type === 'text' || block.type === 'input_text')
271
- && typeof block.text === 'string' && block.text) {
272
- content.push({ type: 'text', text: block.text });
273
- }
274
- }
275
- }
276
- }
277
- if (!content.length) {
278
- const fallbackText = extractModelResponseText(payload);
279
- if (fallbackText) {
280
- content.push({ type: 'text', text: fallbackText });
281
- }
282
- }
283
- return content;
284
- }
285
-
286
- function buildAnthropicStopReasonFromResponses(payload, content) {
287
- if (Array.isArray(content) && content.some((item) => item && item.type === 'tool_use')) {
288
- return 'tool_use';
289
- }
290
- const incompleteReason = payload && payload.incomplete_details && typeof payload.incomplete_details.reason === 'string'
291
- ? payload.incomplete_details.reason
292
- : '';
293
- if (incompleteReason === 'max_output_tokens') {
294
- return 'max_tokens';
295
- }
296
- return 'end_turn';
297
- }
298
-
299
- function buildAnthropicMessageFromResponses(payload, requestPayload = {}) {
300
- const content = collectAnthropicContentFromResponsesOutput(payload);
301
- const usage = buildAnthropicUsageFromResponses(payload);
302
- return {
303
- id: typeof payload.id === 'string' && payload.id.trim()
304
- ? payload.id.trim()
305
- : `msg_${crypto.randomBytes(8).toString('hex')}`,
306
- type: 'message',
307
- role: 'assistant',
308
- model: typeof payload.model === 'string' && payload.model.trim()
309
- ? payload.model.trim()
310
- : (typeof requestPayload.model === 'string' ? requestPayload.model : ''),
311
- content,
312
- stop_reason: buildAnthropicStopReasonFromResponses(payload, content),
313
- stop_sequence: null,
314
- usage
315
- };
316
- }
317
-
318
- function buildAnthropicStreamEvents(message) {
319
- const usage = message && message.usage && typeof message.usage === 'object' ? message.usage : {};
320
- const startUsage = {
321
- input_tokens: readResponsesUsageValue(usage.input_tokens),
322
- output_tokens: 0
323
- };
324
- const events = [{
325
- event: 'message_start',
326
- data: {
327
- type: 'message_start',
328
- message: {
329
- ...message,
330
- content: [],
331
- stop_reason: null,
332
- stop_sequence: null,
333
- usage: startUsage
334
- }
335
- }
336
- }];
337
-
338
- const blocks = Array.isArray(message && message.content) ? message.content : [];
339
- blocks.forEach((block, index) => {
340
- if (!block || typeof block !== 'object') return;
341
- if (block.type === 'text') {
342
- events.push({
343
- event: 'content_block_start',
344
- data: {
345
- type: 'content_block_start',
346
- index,
347
- content_block: { type: 'text', text: '' }
348
- }
349
- });
350
- if (typeof block.text === 'string' && block.text) {
351
- events.push({
352
- event: 'content_block_delta',
353
- data: {
354
- type: 'content_block_delta',
355
- index,
356
- delta: { type: 'text_delta', text: block.text }
357
- }
358
- });
359
- }
360
- events.push({ event: 'content_block_stop', data: { type: 'content_block_stop', index } });
361
- return;
362
- }
363
- if (block.type === 'tool_use') {
364
- events.push({
365
- event: 'content_block_start',
366
- data: {
367
- type: 'content_block_start',
368
- index,
369
- content_block: {
370
- type: 'tool_use',
371
- id: block.id,
372
- name: block.name,
373
- input: {}
374
- }
375
- }
376
- });
377
- const partialJson = safeJsonStringify(block.input && typeof block.input === 'object' ? block.input : {});
378
- if (partialJson && partialJson !== '{}') {
379
- events.push({
380
- event: 'content_block_delta',
381
- data: {
382
- type: 'content_block_delta',
383
- index,
384
- delta: { type: 'input_json_delta', partial_json: partialJson }
385
- }
386
- });
387
- }
388
- events.push({ event: 'content_block_stop', data: { type: 'content_block_stop', index } });
389
- }
390
- });
391
-
392
- events.push({
393
- event: 'message_delta',
394
- data: {
395
- type: 'message_delta',
396
- delta: {
397
- stop_reason: message && message.stop_reason ? message.stop_reason : 'end_turn',
398
- stop_sequence: message && Object.prototype.hasOwnProperty.call(message, 'stop_sequence')
399
- ? message.stop_sequence
400
- : null
401
- },
402
- usage: {
403
- output_tokens: readResponsesUsageValue(usage.output_tokens)
404
- }
405
- }
406
- });
407
- events.push({ event: 'message_stop', data: { type: 'message_stop' } });
408
- return events;
409
- }
410
-
411
- function buildAnthropicModelsPayload(upstreamPayload) {
412
- const ids = extractModelNames(upstreamPayload);
413
- return {
414
- data: ids.map((id) => ({
415
- type: 'model',
416
- id,
417
- display_name: id,
418
- created_at: '1970-01-01T00:00:00Z'
419
- })),
420
- first_id: ids[0] || null,
421
- last_id: ids.length ? ids[ids.length - 1] : null,
422
- has_more: false
423
- };
424
- }
425
-
426
- function createBuiltinClaudeProxyRuntimeController(deps = {}) {
427
- const {
428
- BUILTIN_CLAUDE_PROXY_SETTINGS_FILE,
429
- DEFAULT_BUILTIN_CLAUDE_PROXY_SETTINGS,
430
- BUILTIN_PROXY_PROVIDER_NAME,
431
- MAX_API_BODY_SIZE,
432
- HTTP_KEEP_ALIVE_AGENT,
433
- HTTPS_KEEP_ALIVE_AGENT,
434
- readConfigOrVirtualDefault,
435
- resolveBuiltinProxyProviderName,
436
- resolveAuthTokenFromCurrentProfile
437
- } = deps;
438
-
439
- if (!BUILTIN_CLAUDE_PROXY_SETTINGS_FILE) {
440
- throw new Error('createBuiltinClaudeProxyRuntimeController 缺少 BUILTIN_CLAUDE_PROXY_SETTINGS_FILE');
441
- }
442
- if (!DEFAULT_BUILTIN_CLAUDE_PROXY_SETTINGS || typeof DEFAULT_BUILTIN_CLAUDE_PROXY_SETTINGS !== 'object') {
443
- throw new Error('createBuiltinClaudeProxyRuntimeController 缺少 DEFAULT_BUILTIN_CLAUDE_PROXY_SETTINGS');
444
- }
445
- if (typeof readConfigOrVirtualDefault !== 'function') {
446
- throw new Error('createBuiltinClaudeProxyRuntimeController 缺少 readConfigOrVirtualDefault');
447
- }
448
- if (typeof resolveBuiltinProxyProviderName !== 'function') {
449
- throw new Error('createBuiltinClaudeProxyRuntimeController 缺少 resolveBuiltinProxyProviderName');
450
- }
451
- if (typeof resolveAuthTokenFromCurrentProfile !== 'function') {
452
- throw new Error('createBuiltinClaudeProxyRuntimeController 缺少 resolveAuthTokenFromCurrentProfile');
453
- }
454
-
455
- let runtime = null;
456
-
457
- function normalizeBuiltinClaudeProxySettings(raw) {
458
- const merged = {
459
- ...DEFAULT_BUILTIN_CLAUDE_PROXY_SETTINGS,
460
- ...(isPlainObject(raw) ? raw : {})
461
- };
462
- const host = typeof merged.host === 'string' ? merged.host.trim() : '';
463
- const port = parseInt(String(merged.port), 10);
464
- const provider = typeof merged.provider === 'string' ? merged.provider.trim() : '';
465
- const authSourceRaw = typeof merged.authSource === 'string' ? merged.authSource.trim().toLowerCase() : '';
466
- const timeoutMs = parseInt(String(merged.timeoutMs), 10);
467
- const authSource = authSourceRaw === 'profile' || authSourceRaw === 'none' || authSourceRaw === 'request'
468
- ? authSourceRaw
469
- : 'provider';
470
-
471
- return {
472
- enabled: merged.enabled !== false,
473
- host: host || DEFAULT_BUILTIN_CLAUDE_PROXY_SETTINGS.host,
474
- port: Number.isFinite(port) && port > 0 && port <= 65535 ? port : DEFAULT_BUILTIN_CLAUDE_PROXY_SETTINGS.port,
475
- provider,
476
- authSource,
477
- timeoutMs: Number.isFinite(timeoutMs) && timeoutMs >= 1000
478
- ? timeoutMs
479
- : DEFAULT_BUILTIN_CLAUDE_PROXY_SETTINGS.timeoutMs
480
- };
481
- }
482
-
483
- function readBuiltinClaudeProxySettings() {
484
- const parsed = readJsonFile(BUILTIN_CLAUDE_PROXY_SETTINGS_FILE, null);
485
- return normalizeBuiltinClaudeProxySettings(parsed);
486
- }
487
-
488
- function saveBuiltinClaudeProxySettings(payload = {}, options = {}) {
489
- const current = readBuiltinClaudeProxySettings();
490
- const merged = normalizeBuiltinClaudeProxySettings({
491
- ...current,
492
- ...(isPlainObject(payload) ? payload : {})
493
- });
494
-
495
- if (!merged.host) {
496
- return { error: 'Claude 兼容代理 host 不能为空' };
497
- }
498
- if (!Number.isFinite(merged.port) || merged.port <= 0 || merged.port > 65535) {
499
- return { error: 'Claude 兼容代理端口无效(1-65535)' };
500
- }
501
-
502
- const { config } = readConfigOrVirtualDefault();
503
- const providers = config && isPlainObject(config.model_providers) ? config.model_providers : {};
504
- const preferredProvider = typeof config.model_provider === 'string' ? config.model_provider.trim() : '';
505
- const finalProvider = resolveBuiltinProxyProviderName(merged.provider, providers, preferredProvider);
506
- const normalized = {
507
- ...merged,
508
- provider: finalProvider
509
- };
510
-
511
- if (!options.skipWrite) {
512
- writeJsonAtomic(BUILTIN_CLAUDE_PROXY_SETTINGS_FILE, normalized);
513
- }
514
-
515
- return {
516
- success: true,
517
- settings: normalized
518
- };
519
- }
520
-
521
- function buildBuiltinClaudeProxyListenUrl(settings) {
522
- const host = formatHostForUrl(settings.host || DEFAULT_BUILTIN_CLAUDE_PROXY_SETTINGS.host);
523
- return `http://${host}:${settings.port}`;
524
- }
525
-
526
- function resolveBuiltinClaudeProxyUpstream(settings) {
527
- const { config } = readConfigOrVirtualDefault();
528
- const providers = config && isPlainObject(config.model_providers) ? config.model_providers : {};
529
- const currentProvider = typeof config.model_provider === 'string' ? config.model_provider.trim() : '';
530
- const providerName = resolveBuiltinProxyProviderName(settings.provider, providers, currentProvider);
531
- if (!providerName) {
532
- return { error: '未找到可用的上游 provider,请先添加 responses provider' };
533
- }
534
- if (providerName === BUILTIN_PROXY_PROVIDER_NAME) {
535
- return { error: `Claude 兼容代理的上游 provider 不能是 ${BUILTIN_PROXY_PROVIDER_NAME}` };
536
- }
537
- const provider = providers[providerName];
538
- if (!provider || !isPlainObject(provider)) {
539
- return { error: `上游 provider 不存在: ${providerName}` };
540
- }
541
-
542
- const wireApi = normalizeWireApi(provider.wire_api);
543
- if (wireApi !== 'responses') {
544
- return { error: `Claude 兼容代理仅支持上游 responses provider: ${providerName}` };
545
- }
546
-
547
- const baseUrl = typeof provider.base_url === 'string' ? provider.base_url.trim() : '';
548
- if (!baseUrl || !isValidHttpUrl(baseUrl)) {
549
- return { error: `上游 provider base_url 无效: ${providerName}` };
550
- }
551
-
552
- let token = '';
553
- if (settings.authSource === 'profile') {
554
- token = resolveAuthTokenFromCurrentProfile();
555
- } else if (settings.authSource === 'provider') {
556
- token = typeof provider.preferred_auth_method === 'string' ? provider.preferred_auth_method.trim() : '';
557
- if (!token) {
558
- token = resolveAuthTokenFromCurrentProfile();
559
- }
560
- }
561
-
562
- let authHeader = '';
563
- if (token) {
564
- authHeader = /^bearer\s+/i.test(token) ? token : `Bearer ${token}`;
565
- }
566
-
567
- return {
568
- providerName,
569
- baseUrl: normalizeBaseUrl(baseUrl),
570
- authHeader
571
- };
572
- }
573
-
574
- function buildBuiltinClaudeProxyRequestAuthHeader(req, settings, upstream) {
575
- if (settings && settings.authSource === 'request') {
576
- const apiKey = typeof req.headers['x-api-key'] === 'string'
577
- ? req.headers['x-api-key'].trim()
578
- : '';
579
- if (!apiKey) {
580
- return { error: '缺少 x-api-key,无法转发到上游 responses provider', statusCode: 401 };
581
- }
582
- return {
583
- authHeader: /^bearer\s+/i.test(apiKey) ? apiKey : `Bearer ${apiKey}`
584
- };
585
- }
586
- return { authHeader: upstream.authHeader || '' };
587
- }
588
-
589
- function readJsonRequestBody(req, options = {}) {
590
- const maxBytes = Number.isFinite(options.maxBytes) && options.maxBytes > 0
591
- ? Math.floor(options.maxBytes)
592
- : MAX_API_BODY_SIZE;
593
- return new Promise((resolve, reject) => {
594
- const chunks = [];
595
- let total = 0;
596
- req.on('data', (chunk) => {
597
- total += chunk.length;
598
- if (total > maxBytes) {
599
- reject(new Error(`request body too large (${maxBytes} bytes max)`));
600
- try { req.destroy(); } catch (_) {}
601
- return;
602
- }
603
- chunks.push(chunk);
604
- });
605
- req.on('error', reject);
606
- req.on('end', () => {
607
- const raw = Buffer.concat(chunks).toString('utf-8').trim();
608
- if (!raw) {
609
- resolve({});
610
- return;
611
- }
612
- try {
613
- resolve(JSON.parse(raw));
614
- } catch (e) {
615
- reject(new Error(`invalid JSON body: ${e.message}`));
616
- }
617
- });
618
- });
619
- }
620
-
621
- function extractProxyErrorMessage(payload, fallback = '') {
622
- if (!payload || typeof payload !== 'object') {
623
- return fallback || 'upstream request failed';
624
- }
625
- if (payload.error && typeof payload.error === 'object') {
626
- if (typeof payload.error.message === 'string' && payload.error.message.trim()) {
627
- return payload.error.message.trim();
628
- }
629
- if (typeof payload.error.error === 'string' && payload.error.error.trim()) {
630
- return payload.error.error.trim();
631
- }
632
- }
633
- if (typeof payload.message === 'string' && payload.message.trim()) {
634
- return payload.message.trim();
635
- }
636
- if (typeof payload.error === 'string' && payload.error.trim()) {
637
- return payload.error.trim();
638
- }
639
- return fallback || 'upstream request failed';
640
- }
641
-
642
- function writeAnthropicProxyError(res, statusCode, message, type = 'api_error') {
643
- const body = JSON.stringify({
644
- type: 'error',
645
- error: {
646
- type,
647
- message: typeof message === 'string' && message.trim() ? message.trim() : 'request failed'
648
- }
649
- });
650
- res.writeHead(statusCode, {
651
- 'Content-Type': 'application/json; charset=utf-8',
652
- 'Content-Length': Buffer.byteLength(body, 'utf-8')
653
- });
654
- res.end(body, 'utf-8');
655
- }
656
-
657
- function requestBuiltinClaudeProxyUpstream(upstream, requestOptions = {}) {
658
- const pathSuffix = typeof requestOptions.pathSuffix === 'string' ? requestOptions.pathSuffix : '';
659
- const targetBase = joinApiUrl(upstream.baseUrl, pathSuffix);
660
- if (!targetBase) {
661
- return Promise.reject(new Error('failed to build upstream URL'));
662
- }
663
-
664
- let targetUrl;
665
- try {
666
- targetUrl = new URL(targetBase);
667
- } catch (e) {
668
- return Promise.reject(new Error(`invalid upstream URL: ${e.message}`));
669
- }
670
-
671
- const bodyText = requestOptions.body === undefined ? '' : JSON.stringify(requestOptions.body);
672
- const headers = {
673
- Accept: 'application/json',
674
- ...(isPlainObject(requestOptions.headers) ? requestOptions.headers : {})
675
- };
676
- if (bodyText) {
677
- headers['Content-Type'] = 'application/json';
678
- headers['Content-Length'] = Buffer.byteLength(bodyText);
679
- }
680
- if (requestOptions.authHeader) {
681
- headers.authorization = requestOptions.authHeader;
682
- }
683
- headers['x-codexmate-claude-proxy'] = '1';
684
-
685
- const transport = targetUrl.protocol === 'https:' ? https : http;
686
- const timeoutMs = Number.isFinite(requestOptions.timeoutMs) && requestOptions.timeoutMs > 0
687
- ? requestOptions.timeoutMs
688
- : DEFAULT_BUILTIN_CLAUDE_PROXY_SETTINGS.timeoutMs;
689
-
690
- return new Promise((resolve, reject) => {
691
- const upstreamReq = transport.request({
692
- protocol: targetUrl.protocol,
693
- hostname: targetUrl.hostname,
694
- port: targetUrl.port || (targetUrl.protocol === 'https:' ? 443 : 80),
695
- method: requestOptions.method || 'POST',
696
- path: `${targetUrl.pathname}${targetUrl.search}`,
697
- headers,
698
- agent: targetUrl.protocol === 'https:' ? HTTPS_KEEP_ALIVE_AGENT : HTTP_KEEP_ALIVE_AGENT
699
- }, (upstreamRes) => {
700
- const chunks = [];
701
- let total = 0;
702
- upstreamRes.on('data', (chunk) => {
703
- total += chunk.length;
704
- if (total > MAX_API_BODY_SIZE) {
705
- upstreamReq.destroy(new Error(`upstream body too large (${MAX_API_BODY_SIZE} bytes max)`));
706
- return;
707
- }
708
- chunks.push(chunk);
709
- });
710
- upstreamRes.on('error', reject);
711
- upstreamRes.on('end', () => {
712
- const rawBody = Buffer.concat(chunks).toString('utf-8');
713
- let payload = null;
714
- if (rawBody.trim()) {
715
- try {
716
- payload = JSON.parse(rawBody);
717
- } catch (_) {
718
- payload = null;
719
- }
720
- }
721
- resolve({
722
- statusCode: upstreamRes.statusCode || 502,
723
- headers: upstreamRes.headers,
724
- rawBody,
725
- payload
726
- });
727
- });
728
- });
729
-
730
- upstreamReq.setTimeout(timeoutMs, () => {
731
- upstreamReq.destroy(new Error(`upstream timeout (${timeoutMs}ms)`));
732
- });
733
- upstreamReq.on('error', reject);
734
- if (bodyText) {
735
- upstreamReq.write(bodyText, 'utf-8');
736
- }
737
- upstreamReq.end();
738
- });
739
- }
740
-
741
- function writeAnthropicStreamEvents(res, message) {
742
- const events = buildAnthropicStreamEvents(message);
743
- res.writeHead(200, {
744
- 'Content-Type': 'text/event-stream; charset=utf-8',
745
- 'Cache-Control': 'no-cache, no-transform',
746
- Connection: 'keep-alive',
747
- 'X-Accel-Buffering': 'no'
748
- });
749
- for (const event of events) {
750
- if (event && event.event) {
751
- res.write(`event: ${event.event}\n`);
752
- }
753
- res.write(`data: ${JSON.stringify(event && event.data ? event.data : {})}\n\n`);
754
- }
755
- res.end();
756
- }
757
-
758
- async function handleBuiltinClaudeProxyRequest(req, res, settings, upstream) {
759
- let parsedIncoming;
760
- try {
761
- parsedIncoming = new URL(req.url || '/', 'http://localhost');
762
- } catch (e) {
763
- writeAnthropicProxyError(res, 400, 'invalid request path', 'invalid_request_error');
764
- return;
765
- }
766
-
767
- const incomingPath = parsedIncoming.pathname || '/';
768
- if (incomingPath === '/health' || incomingPath === '/status') {
769
- const body = JSON.stringify({
770
- ok: true,
771
- upstreamProvider: upstream.providerName,
772
- upstreamBaseUrl: upstream.baseUrl,
773
- mode: 'anthropic-to-responses'
774
- });
775
- res.writeHead(200, {
776
- 'Content-Type': 'application/json; charset=utf-8',
777
- 'Content-Length': Buffer.byteLength(body, 'utf-8')
778
- });
779
- res.end(body, 'utf-8');
780
- return;
781
- }
782
-
783
- const authResult = buildBuiltinClaudeProxyRequestAuthHeader(req, settings, upstream);
784
- if (authResult.error) {
785
- writeAnthropicProxyError(res, authResult.statusCode || 401, authResult.error, 'authentication_error');
786
- return;
787
- }
788
-
789
- if (incomingPath === '/v1/models') {
790
- if ((req.method || 'GET').toUpperCase() !== 'GET') {
791
- res.writeHead(405, { Allow: 'GET' });
792
- res.end();
793
- return;
794
- }
795
- const upstreamResponse = await requestBuiltinClaudeProxyUpstream(upstream, {
796
- method: 'GET',
797
- pathSuffix: 'models',
798
- authHeader: authResult.authHeader,
799
- timeoutMs: settings.timeoutMs
800
- });
801
- if (upstreamResponse.statusCode < 200 || upstreamResponse.statusCode >= 300) {
802
- writeAnthropicProxyError(
803
- res,
804
- upstreamResponse.statusCode,
805
- extractProxyErrorMessage(upstreamResponse.payload, upstreamResponse.rawBody),
806
- 'api_error'
807
- );
808
- return;
809
- }
810
- const body = JSON.stringify(buildAnthropicModelsPayload(upstreamResponse.payload));
811
- res.writeHead(200, {
812
- 'Content-Type': 'application/json; charset=utf-8',
813
- 'Content-Length': Buffer.byteLength(body, 'utf-8')
814
- });
815
- res.end(body, 'utf-8');
816
- return;
817
- }
818
-
819
- if (incomingPath !== '/v1/messages') {
820
- writeAnthropicProxyError(res, 404, 'Claude 兼容代理仅支持 /v1/messages 与 /v1/models', 'not_found_error');
821
- return;
822
- }
823
-
824
- if ((req.method || 'POST').toUpperCase() !== 'POST') {
825
- res.writeHead(405, { Allow: 'POST' });
826
- res.end();
827
- return;
828
- }
829
-
830
- const payload = await readJsonRequestBody(req);
831
- const upstreamRequestBody = buildBuiltinClaudeResponsesRequest(payload);
832
- const upstreamResponse = await requestBuiltinClaudeProxyUpstream(upstream, {
833
- method: 'POST',
834
- pathSuffix: 'responses',
835
- body: upstreamRequestBody,
836
- authHeader: authResult.authHeader,
837
- timeoutMs: settings.timeoutMs
838
- });
839
-
840
- if (upstreamResponse.statusCode < 200 || upstreamResponse.statusCode >= 300) {
841
- writeAnthropicProxyError(
842
- res,
843
- upstreamResponse.statusCode,
844
- extractProxyErrorMessage(upstreamResponse.payload, upstreamResponse.rawBody),
845
- 'api_error'
846
- );
847
- return;
848
- }
849
-
850
- const anthropicMessage = buildAnthropicMessageFromResponses(upstreamResponse.payload || {}, payload);
851
- if (payload.stream === true) {
852
- writeAnthropicStreamEvents(res, anthropicMessage);
853
- return;
854
- }
855
-
856
- const body = JSON.stringify(anthropicMessage);
857
- res.writeHead(200, {
858
- 'Content-Type': 'application/json; charset=utf-8',
859
- 'Content-Length': Buffer.byteLength(body, 'utf-8')
860
- });
861
- res.end(body, 'utf-8');
862
- }
863
-
864
- function createBuiltinClaudeProxyServer(settings, upstream) {
865
- const connections = new Set();
866
- const server = http.createServer((req, res) => {
867
- const remoteAddr = req && req.socket ? req.socket.remoteAddress : '';
868
- const isLoopback = !remoteAddr
869
- || remoteAddr === '127.0.0.1'
870
- || remoteAddr === '::1'
871
- || remoteAddr === '::ffff:127.0.0.1';
872
- if (!isLoopback) {
873
- const expected = typeof process.env.CODEXMATE_HTTP_TOKEN === 'string'
874
- ? process.env.CODEXMATE_HTTP_TOKEN.trim()
875
- : '';
876
- if (!expected) {
877
- writeAnthropicProxyError(res, 403, 'Remote access is disabled (set CODEXMATE_HTTP_TOKEN)', 'authentication_error');
878
- return;
879
- }
880
- const headers = req && req.headers && typeof req.headers === 'object' ? req.headers : {};
881
- const rawAuth = typeof headers.authorization === 'string' ? headers.authorization.trim() : '';
882
- const match = rawAuth ? rawAuth.match(/^bearer\s+(.+)$/i) : null;
883
- const actual = match && match[1]
884
- ? match[1].trim()
885
- : (rawAuth ? rawAuth : (typeof headers['x-codexmate-token'] === 'string' ? String(headers['x-codexmate-token']).trim() : ''));
886
- if (!actual || actual !== expected) {
887
- writeAnthropicProxyError(res, 401, 'Unauthorized', 'authentication_error');
888
- return;
889
- }
890
- }
891
- handleBuiltinClaudeProxyRequest(req, res, settings, upstream).catch((err) => {
892
- if (res.headersSent) {
893
- try { res.destroy(err); } catch (_) {}
894
- return;
895
- }
896
- writeAnthropicProxyError(res, 502, `claude proxy request failed: ${err.message}`, 'api_error');
897
- });
898
- });
899
-
900
- server.on('connection', (socket) => {
901
- connections.add(socket);
902
- socket.on('close', () => connections.delete(socket));
903
- });
904
-
905
- return new Promise((resolve, reject) => {
906
- server.once('error', reject);
907
- server.listen(settings.port, settings.host, () => {
908
- server.removeListener('error', reject);
909
- resolve({
910
- server,
911
- connections,
912
- settings,
913
- upstream,
914
- startedAt: toIsoTime(Date.now()),
915
- listenUrl: buildBuiltinClaudeProxyListenUrl(settings)
916
- });
917
- });
918
- });
919
- }
920
-
921
- async function startBuiltinClaudeProxyRuntime(payload = {}) {
922
- if (runtime) {
923
- return {
924
- error: 'Claude 兼容代理已在运行',
925
- runtime: {
926
- listenUrl: runtime.listenUrl,
927
- upstreamProvider: runtime.upstream.providerName
928
- }
929
- };
930
- }
931
-
932
- const saveResult = saveBuiltinClaudeProxySettings(payload);
933
- if (saveResult.error) {
934
- return { error: saveResult.error };
935
- }
936
- const settings = saveResult.settings;
937
- const upstream = resolveBuiltinClaudeProxyUpstream(settings);
938
- if (upstream.error) {
939
- return { error: upstream.error };
940
- }
941
-
942
- try {
943
- runtime = await createBuiltinClaudeProxyServer(settings, upstream);
944
- return {
945
- success: true,
946
- running: true,
947
- listenUrl: runtime.listenUrl,
948
- upstreamProvider: upstream.providerName,
949
- mode: 'anthropic-to-responses',
950
- settings
951
- };
952
- } catch (e) {
953
- return { error: `启动 Claude 兼容代理失败: ${e.message}` };
954
- }
955
- }
956
-
957
- async function stopBuiltinClaudeProxyRuntime() {
958
- if (!runtime) {
959
- return { success: true, running: false };
960
- }
961
- const currentRuntime = runtime;
962
- runtime = null;
963
-
964
- await new Promise((resolve) => {
965
- let settled = false;
966
- const finish = () => {
967
- if (settled) return;
968
- settled = true;
969
- resolve();
970
- };
971
-
972
- currentRuntime.server.close(() => finish());
973
- setTimeout(() => finish(), 1000);
974
- });
975
-
976
- for (const socket of currentRuntime.connections) {
977
- try { socket.destroy(); } catch (_) {}
978
- }
979
- currentRuntime.connections.clear();
980
-
981
- return {
982
- success: true,
983
- running: false
984
- };
985
- }
986
-
987
- function getBuiltinClaudeProxyStatus() {
988
- const settings = readBuiltinClaudeProxySettings();
989
- return {
990
- running: !!runtime,
991
- settings,
992
- runtime: runtime
993
- ? {
994
- startedAt: runtime.startedAt,
995
- listenUrl: runtime.listenUrl,
996
- upstreamProvider: runtime.upstream.providerName,
997
- upstreamBaseUrl: runtime.upstream.baseUrl,
998
- mode: 'anthropic-to-responses'
999
- }
1000
- : null
1001
- };
1002
- }
1003
-
1004
- return {
1005
- normalizeBuiltinClaudeProxySettings,
1006
- readBuiltinClaudeProxySettings,
1007
- saveBuiltinClaudeProxySettings,
1008
- buildBuiltinClaudeProxyListenUrl,
1009
- resolveBuiltinClaudeProxyUpstream,
1010
- startBuiltinClaudeProxyRuntime,
1011
- stopBuiltinClaudeProxyRuntime,
1012
- getBuiltinClaudeProxyStatus
1013
- };
1014
- }
1015
-
1016
- module.exports = {
1017
- createBuiltinClaudeProxyRuntimeController,
1018
- buildBuiltinClaudeResponsesRequest,
1019
- buildAnthropicMessageFromResponses,
1020
- buildAnthropicStreamEvents,
1021
- buildAnthropicModelsPayload
1022
- };
1
+ const http = require('http');
2
+ const https = require('https');
3
+ const crypto = require('crypto');
4
+ const { readJsonFile, writeJsonAtomic } = require('../lib/cli-file-utils');
5
+ const { isValidHttpUrl, normalizeBaseUrl, joinApiUrl } = require('../lib/cli-utils');
6
+ const {
7
+ extractModelNames,
8
+ extractModelResponseText,
9
+ normalizeWireApi
10
+ } = require('../lib/cli-models-utils');
11
+ const { toIsoTime } = require('../lib/cli-session-utils');
12
+
13
+ function isPlainObject(value) {
14
+ return !!value && typeof value === 'object' && !Array.isArray(value);
15
+ }
16
+
17
+ function readNonNegativeInteger(value) {
18
+ if (typeof value === 'number' && Number.isFinite(value) && value >= 0) {
19
+ return Math.floor(value);
20
+ }
21
+ if (typeof value === 'string' && value.trim()) {
22
+ const parsed = Number.parseInt(value.trim(), 10);
23
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : 0;
24
+ }
25
+ return 0;
26
+ }
27
+
28
+ function formatHostForUrl(host) {
29
+ const value = typeof host === 'string' ? host.trim() : '';
30
+ if (!value) return '';
31
+ if (value.startsWith('[') && value.endsWith(']')) {
32
+ return value;
33
+ }
34
+ if (value.includes(':')) {
35
+ return `[${value}]`;
36
+ }
37
+ return value;
38
+ }
39
+
40
+ function normalizeAnthropicContentBlocks(content) {
41
+ if (typeof content === 'string') {
42
+ return content.trim() ? [{ type: 'text', text: content }] : [];
43
+ }
44
+ if (Array.isArray(content)) {
45
+ return content.flatMap((item) => normalizeAnthropicContentBlocks(item));
46
+ }
47
+ if (content && typeof content === 'object') {
48
+ if (typeof content.type === 'string') {
49
+ return [content];
50
+ }
51
+ if (typeof content.text === 'string') {
52
+ return [{ type: 'text', text: content.text }];
53
+ }
54
+ }
55
+ return [];
56
+ }
57
+
58
+ function collectAnthropicTextContent(content) {
59
+ const pieces = [];
60
+ for (const block of normalizeAnthropicContentBlocks(content)) {
61
+ if (block && block.type === 'text' && typeof block.text === 'string' && block.text.trim()) {
62
+ pieces.push(block.text.trim());
63
+ }
64
+ }
65
+ return pieces.join('\n\n').trim();
66
+ }
67
+
68
+ function safeJsonStringify(value) {
69
+ try {
70
+ return JSON.stringify(value);
71
+ } catch (e) {
72
+ return JSON.stringify(String(value));
73
+ }
74
+ }
75
+
76
+ function stringifyAnthropicToolResultContent(content) {
77
+ if (typeof content === 'string') {
78
+ return content;
79
+ }
80
+ const text = collectAnthropicTextContent(content);
81
+ if (text) {
82
+ return text;
83
+ }
84
+ return safeJsonStringify(content);
85
+ }
86
+
87
+ function appendAnthropicMessageToResponsesInput(target, message) {
88
+ if (!message || typeof message !== 'object') return;
89
+ const roleRaw = typeof message.role === 'string' ? message.role.trim().toLowerCase() : '';
90
+ const role = roleRaw === 'assistant' ? 'assistant' : 'user';
91
+ const textType = role === 'assistant' ? 'output_text' : 'input_text';
92
+ let buffered = [];
93
+
94
+ const flushBuffered = () => {
95
+ if (!buffered.length) return;
96
+ target.push({ role, content: buffered });
97
+ buffered = [];
98
+ };
99
+
100
+ for (const block of normalizeAnthropicContentBlocks(message.content)) {
101
+ if (!block || typeof block !== 'object') continue;
102
+ if (block.type === 'text' && typeof block.text === 'string' && block.text) {
103
+ buffered.push({ type: textType, text: block.text });
104
+ continue;
105
+ }
106
+ if (block.type === 'tool_use' && typeof block.name === 'string' && block.name.trim()) {
107
+ flushBuffered();
108
+ target.push({
109
+ type: 'function_call',
110
+ call_id: typeof block.id === 'string' && block.id.trim()
111
+ ? block.id.trim()
112
+ : `call_${crypto.randomBytes(8).toString('hex')}`,
113
+ name: block.name.trim(),
114
+ arguments: safeJsonStringify(block.input && typeof block.input === 'object' ? block.input : {})
115
+ });
116
+ continue;
117
+ }
118
+ if (block.type === 'tool_result' && typeof block.tool_use_id === 'string' && block.tool_use_id.trim()) {
119
+ flushBuffered();
120
+ target.push({
121
+ type: 'function_call_output',
122
+ call_id: block.tool_use_id.trim(),
123
+ output: stringifyAnthropicToolResultContent(block.content)
124
+ });
125
+ continue;
126
+ }
127
+ buffered.push({
128
+ type: textType,
129
+ text: `[unsupported anthropic block: ${typeof block.type === 'string' ? block.type : 'unknown'}]`
130
+ });
131
+ }
132
+
133
+ flushBuffered();
134
+ }
135
+
136
+ function mapAnthropicToolChoiceToResponses(toolChoice) {
137
+ if (!toolChoice) return undefined;
138
+ if (typeof toolChoice === 'string') {
139
+ if (toolChoice === 'auto') return 'auto';
140
+ if (toolChoice === 'any') return 'required';
141
+ return undefined;
142
+ }
143
+ if (!toolChoice || typeof toolChoice !== 'object') return undefined;
144
+ const type = typeof toolChoice.type === 'string' ? toolChoice.type.trim().toLowerCase() : '';
145
+ if (type === 'auto') return 'auto';
146
+ if (type === 'any') return 'required';
147
+ if (type === 'tool' && typeof toolChoice.name === 'string' && toolChoice.name.trim()) {
148
+ return {
149
+ type: 'function',
150
+ name: toolChoice.name.trim()
151
+ };
152
+ }
153
+ return undefined;
154
+ }
155
+
156
+ function buildBuiltinClaudeResponsesRequest(payload = {}) {
157
+ const model = typeof payload.model === 'string' ? payload.model.trim() : '';
158
+ if (!model) {
159
+ throw new Error('Anthropic messages 请求缺少 model');
160
+ }
161
+ const messages = Array.isArray(payload.messages) ? payload.messages : [];
162
+ if (!messages.length) {
163
+ throw new Error('Anthropic messages 请求缺少 messages');
164
+ }
165
+
166
+ const maxTokens = parseInt(String(payload.max_tokens), 10);
167
+ const requestBody = {
168
+ model,
169
+ input: [],
170
+ max_output_tokens: Number.isFinite(maxTokens) && maxTokens > 0 ? maxTokens : 1024
171
+ };
172
+
173
+ const instructions = collectAnthropicTextContent(payload.system);
174
+ if (instructions) {
175
+ requestBody.instructions = instructions;
176
+ }
177
+
178
+ for (const message of messages) {
179
+ appendAnthropicMessageToResponsesInput(requestBody.input, message);
180
+ }
181
+
182
+ if (Number.isFinite(payload.temperature)) {
183
+ requestBody.temperature = Number(payload.temperature);
184
+ }
185
+ if (Number.isFinite(payload.top_p)) {
186
+ requestBody.top_p = Number(payload.top_p);
187
+ }
188
+ if (Array.isArray(payload.stop_sequences) && payload.stop_sequences.length) {
189
+ requestBody.stop = payload.stop_sequences.filter((item) => typeof item === 'string' && item.trim());
190
+ }
191
+ if (isPlainObject(payload.metadata)) {
192
+ requestBody.metadata = payload.metadata;
193
+ }
194
+ if (Array.isArray(payload.tools) && payload.tools.length) {
195
+ requestBody.tools = payload.tools
196
+ .map((tool) => {
197
+ if (!tool || typeof tool !== 'object') return null;
198
+ const name = typeof tool.name === 'string' ? tool.name.trim() : '';
199
+ if (!name) return null;
200
+ return {
201
+ type: 'function',
202
+ name,
203
+ description: typeof tool.description === 'string' ? tool.description : '',
204
+ parameters: isPlainObject(tool.input_schema) ? tool.input_schema : { type: 'object', properties: {} }
205
+ };
206
+ })
207
+ .filter(Boolean);
208
+ if (!requestBody.tools.length) {
209
+ delete requestBody.tools;
210
+ }
211
+ }
212
+
213
+ const toolChoice = mapAnthropicToolChoiceToResponses(payload.tool_choice);
214
+ if (toolChoice !== undefined) {
215
+ requestBody.tool_choice = toolChoice;
216
+ }
217
+
218
+ return requestBody;
219
+ }
220
+
221
+ function parseJsonObjectLoose(value) {
222
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
223
+ return value;
224
+ }
225
+ if (typeof value !== 'string' || !value.trim()) {
226
+ return {};
227
+ }
228
+ try {
229
+ const parsed = JSON.parse(value);
230
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
231
+ } catch (e) {
232
+ return {};
233
+ }
234
+ }
235
+
236
+ function readResponsesUsageValue(value) {
237
+ const parsed = readNonNegativeInteger(value);
238
+ return parsed > 0 ? parsed : 0;
239
+ }
240
+
241
+ function buildAnthropicUsageFromResponses(payload) {
242
+ const usage = payload && payload.usage && typeof payload.usage === 'object' ? payload.usage : {};
243
+ return {
244
+ input_tokens: readResponsesUsageValue(usage.input_tokens),
245
+ output_tokens: readResponsesUsageValue(usage.output_tokens)
246
+ };
247
+ }
248
+
249
+ function collectAnthropicContentFromResponsesOutput(payload) {
250
+ const content = [];
251
+ const output = Array.isArray(payload && payload.output) ? payload.output : [];
252
+ for (const item of output) {
253
+ if (!item || typeof item !== 'object') continue;
254
+ if (item.type === 'function_call') {
255
+ content.push({
256
+ type: 'tool_use',
257
+ id: typeof item.call_id === 'string' && item.call_id.trim()
258
+ ? item.call_id.trim()
259
+ : (typeof item.id === 'string' && item.id.trim()
260
+ ? item.id.trim()
261
+ : `toolu_${crypto.randomBytes(8).toString('hex')}`),
262
+ name: typeof item.name === 'string' ? item.name : '',
263
+ input: parseJsonObjectLoose(item.arguments)
264
+ });
265
+ continue;
266
+ }
267
+ if (item.type === 'message' && Array.isArray(item.content)) {
268
+ for (const block of item.content) {
269
+ if (!block || typeof block !== 'object') continue;
270
+ if ((block.type === 'output_text' || block.type === 'text' || block.type === 'input_text')
271
+ && typeof block.text === 'string' && block.text) {
272
+ content.push({ type: 'text', text: block.text });
273
+ }
274
+ }
275
+ }
276
+ }
277
+ if (!content.length) {
278
+ const fallbackText = extractModelResponseText(payload);
279
+ if (fallbackText) {
280
+ content.push({ type: 'text', text: fallbackText });
281
+ }
282
+ }
283
+ return content;
284
+ }
285
+
286
+ function buildAnthropicStopReasonFromResponses(payload, content) {
287
+ if (Array.isArray(content) && content.some((item) => item && item.type === 'tool_use')) {
288
+ return 'tool_use';
289
+ }
290
+ const incompleteReason = payload && payload.incomplete_details && typeof payload.incomplete_details.reason === 'string'
291
+ ? payload.incomplete_details.reason
292
+ : '';
293
+ if (incompleteReason === 'max_output_tokens') {
294
+ return 'max_tokens';
295
+ }
296
+ return 'end_turn';
297
+ }
298
+
299
+ function buildAnthropicMessageFromResponses(payload, requestPayload = {}) {
300
+ const content = collectAnthropicContentFromResponsesOutput(payload);
301
+ const usage = buildAnthropicUsageFromResponses(payload);
302
+ return {
303
+ id: typeof payload.id === 'string' && payload.id.trim()
304
+ ? payload.id.trim()
305
+ : `msg_${crypto.randomBytes(8).toString('hex')}`,
306
+ type: 'message',
307
+ role: 'assistant',
308
+ model: typeof payload.model === 'string' && payload.model.trim()
309
+ ? payload.model.trim()
310
+ : (typeof requestPayload.model === 'string' ? requestPayload.model : ''),
311
+ content,
312
+ stop_reason: buildAnthropicStopReasonFromResponses(payload, content),
313
+ stop_sequence: null,
314
+ usage
315
+ };
316
+ }
317
+
318
+ function buildAnthropicStreamEvents(message) {
319
+ const usage = message && message.usage && typeof message.usage === 'object' ? message.usage : {};
320
+ const startUsage = {
321
+ input_tokens: readResponsesUsageValue(usage.input_tokens),
322
+ output_tokens: 0
323
+ };
324
+ const events = [{
325
+ event: 'message_start',
326
+ data: {
327
+ type: 'message_start',
328
+ message: {
329
+ ...message,
330
+ content: [],
331
+ stop_reason: null,
332
+ stop_sequence: null,
333
+ usage: startUsage
334
+ }
335
+ }
336
+ }];
337
+
338
+ const blocks = Array.isArray(message && message.content) ? message.content : [];
339
+ blocks.forEach((block, index) => {
340
+ if (!block || typeof block !== 'object') return;
341
+ if (block.type === 'text') {
342
+ events.push({
343
+ event: 'content_block_start',
344
+ data: {
345
+ type: 'content_block_start',
346
+ index,
347
+ content_block: { type: 'text', text: '' }
348
+ }
349
+ });
350
+ if (typeof block.text === 'string' && block.text) {
351
+ events.push({
352
+ event: 'content_block_delta',
353
+ data: {
354
+ type: 'content_block_delta',
355
+ index,
356
+ delta: { type: 'text_delta', text: block.text }
357
+ }
358
+ });
359
+ }
360
+ events.push({ event: 'content_block_stop', data: { type: 'content_block_stop', index } });
361
+ return;
362
+ }
363
+ if (block.type === 'tool_use') {
364
+ events.push({
365
+ event: 'content_block_start',
366
+ data: {
367
+ type: 'content_block_start',
368
+ index,
369
+ content_block: {
370
+ type: 'tool_use',
371
+ id: block.id,
372
+ name: block.name,
373
+ input: {}
374
+ }
375
+ }
376
+ });
377
+ const partialJson = safeJsonStringify(block.input && typeof block.input === 'object' ? block.input : {});
378
+ if (partialJson && partialJson !== '{}') {
379
+ events.push({
380
+ event: 'content_block_delta',
381
+ data: {
382
+ type: 'content_block_delta',
383
+ index,
384
+ delta: { type: 'input_json_delta', partial_json: partialJson }
385
+ }
386
+ });
387
+ }
388
+ events.push({ event: 'content_block_stop', data: { type: 'content_block_stop', index } });
389
+ }
390
+ });
391
+
392
+ events.push({
393
+ event: 'message_delta',
394
+ data: {
395
+ type: 'message_delta',
396
+ delta: {
397
+ stop_reason: message && message.stop_reason ? message.stop_reason : 'end_turn',
398
+ stop_sequence: message && Object.prototype.hasOwnProperty.call(message, 'stop_sequence')
399
+ ? message.stop_sequence
400
+ : null
401
+ },
402
+ usage: {
403
+ output_tokens: readResponsesUsageValue(usage.output_tokens)
404
+ }
405
+ }
406
+ });
407
+ events.push({ event: 'message_stop', data: { type: 'message_stop' } });
408
+ return events;
409
+ }
410
+
411
+ function buildAnthropicModelsPayload(upstreamPayload) {
412
+ const ids = extractModelNames(upstreamPayload);
413
+ return {
414
+ data: ids.map((id) => ({
415
+ type: 'model',
416
+ id,
417
+ display_name: id,
418
+ created_at: '1970-01-01T00:00:00Z'
419
+ })),
420
+ first_id: ids[0] || null,
421
+ last_id: ids.length ? ids[ids.length - 1] : null,
422
+ has_more: false
423
+ };
424
+ }
425
+
426
+ function createBuiltinClaudeProxyRuntimeController(deps = {}) {
427
+ const {
428
+ BUILTIN_CLAUDE_PROXY_SETTINGS_FILE,
429
+ DEFAULT_BUILTIN_CLAUDE_PROXY_SETTINGS,
430
+ BUILTIN_PROXY_PROVIDER_NAME,
431
+ MAX_API_BODY_SIZE,
432
+ HTTP_KEEP_ALIVE_AGENT,
433
+ HTTPS_KEEP_ALIVE_AGENT,
434
+ readConfigOrVirtualDefault,
435
+ resolveBuiltinProxyProviderName,
436
+ resolveAuthTokenFromCurrentProfile
437
+ } = deps;
438
+
439
+ if (!BUILTIN_CLAUDE_PROXY_SETTINGS_FILE) {
440
+ throw new Error('createBuiltinClaudeProxyRuntimeController 缺少 BUILTIN_CLAUDE_PROXY_SETTINGS_FILE');
441
+ }
442
+ if (!DEFAULT_BUILTIN_CLAUDE_PROXY_SETTINGS || typeof DEFAULT_BUILTIN_CLAUDE_PROXY_SETTINGS !== 'object') {
443
+ throw new Error('createBuiltinClaudeProxyRuntimeController 缺少 DEFAULT_BUILTIN_CLAUDE_PROXY_SETTINGS');
444
+ }
445
+ if (typeof readConfigOrVirtualDefault !== 'function') {
446
+ throw new Error('createBuiltinClaudeProxyRuntimeController 缺少 readConfigOrVirtualDefault');
447
+ }
448
+ if (typeof resolveBuiltinProxyProviderName !== 'function') {
449
+ throw new Error('createBuiltinClaudeProxyRuntimeController 缺少 resolveBuiltinProxyProviderName');
450
+ }
451
+ if (typeof resolveAuthTokenFromCurrentProfile !== 'function') {
452
+ throw new Error('createBuiltinClaudeProxyRuntimeController 缺少 resolveAuthTokenFromCurrentProfile');
453
+ }
454
+
455
+ let runtime = null;
456
+
457
+ function normalizeBuiltinClaudeProxySettings(raw) {
458
+ const merged = {
459
+ ...DEFAULT_BUILTIN_CLAUDE_PROXY_SETTINGS,
460
+ ...(isPlainObject(raw) ? raw : {})
461
+ };
462
+ const host = typeof merged.host === 'string' ? merged.host.trim() : '';
463
+ const port = parseInt(String(merged.port), 10);
464
+ const provider = typeof merged.provider === 'string' ? merged.provider.trim() : '';
465
+ const authSourceRaw = typeof merged.authSource === 'string' ? merged.authSource.trim().toLowerCase() : '';
466
+ const timeoutMs = parseInt(String(merged.timeoutMs), 10);
467
+ const authSource = authSourceRaw === 'profile' || authSourceRaw === 'none' || authSourceRaw === 'request'
468
+ ? authSourceRaw
469
+ : 'provider';
470
+
471
+ return {
472
+ enabled: merged.enabled !== false,
473
+ host: host || DEFAULT_BUILTIN_CLAUDE_PROXY_SETTINGS.host,
474
+ port: Number.isFinite(port) && port > 0 && port <= 65535 ? port : DEFAULT_BUILTIN_CLAUDE_PROXY_SETTINGS.port,
475
+ provider,
476
+ authSource,
477
+ timeoutMs: Number.isFinite(timeoutMs) && timeoutMs >= 1000
478
+ ? timeoutMs
479
+ : DEFAULT_BUILTIN_CLAUDE_PROXY_SETTINGS.timeoutMs
480
+ };
481
+ }
482
+
483
+ function readBuiltinClaudeProxySettings() {
484
+ const parsed = readJsonFile(BUILTIN_CLAUDE_PROXY_SETTINGS_FILE, null);
485
+ return normalizeBuiltinClaudeProxySettings(parsed);
486
+ }
487
+
488
+ function saveBuiltinClaudeProxySettings(payload = {}, options = {}) {
489
+ const current = readBuiltinClaudeProxySettings();
490
+ const merged = normalizeBuiltinClaudeProxySettings({
491
+ ...current,
492
+ ...(isPlainObject(payload) ? payload : {})
493
+ });
494
+
495
+ if (!merged.host) {
496
+ return { error: 'Claude 兼容代理 host 不能为空' };
497
+ }
498
+ if (!Number.isFinite(merged.port) || merged.port <= 0 || merged.port > 65535) {
499
+ return { error: 'Claude 兼容代理端口无效(1-65535)' };
500
+ }
501
+
502
+ const { config } = readConfigOrVirtualDefault();
503
+ const providers = config && isPlainObject(config.model_providers) ? config.model_providers : {};
504
+ const preferredProvider = typeof config.model_provider === 'string' ? config.model_provider.trim() : '';
505
+ const finalProvider = resolveBuiltinProxyProviderName(merged.provider, providers, preferredProvider);
506
+ const normalized = {
507
+ ...merged,
508
+ provider: finalProvider
509
+ };
510
+
511
+ if (!options.skipWrite) {
512
+ writeJsonAtomic(BUILTIN_CLAUDE_PROXY_SETTINGS_FILE, normalized);
513
+ }
514
+
515
+ return {
516
+ success: true,
517
+ settings: normalized
518
+ };
519
+ }
520
+
521
+ function buildBuiltinClaudeProxyListenUrl(settings) {
522
+ const host = formatHostForUrl(settings.host || DEFAULT_BUILTIN_CLAUDE_PROXY_SETTINGS.host);
523
+ return `http://${host}:${settings.port}`;
524
+ }
525
+
526
+ function resolveBuiltinClaudeProxyUpstream(settings) {
527
+ const { config } = readConfigOrVirtualDefault();
528
+ const providers = config && isPlainObject(config.model_providers) ? config.model_providers : {};
529
+ const currentProvider = typeof config.model_provider === 'string' ? config.model_provider.trim() : '';
530
+ const providerName = resolveBuiltinProxyProviderName(settings.provider, providers, currentProvider);
531
+ if (!providerName) {
532
+ return { error: '未找到可用的上游 provider,请先添加 responses provider' };
533
+ }
534
+ if (providerName === BUILTIN_PROXY_PROVIDER_NAME) {
535
+ return { error: `Claude 兼容代理的上游 provider 不能是 ${BUILTIN_PROXY_PROVIDER_NAME}` };
536
+ }
537
+ const provider = providers[providerName];
538
+ if (!provider || !isPlainObject(provider)) {
539
+ return { error: `上游 provider 不存在: ${providerName}` };
540
+ }
541
+
542
+ const wireApi = normalizeWireApi(provider.wire_api);
543
+ if (wireApi !== 'responses') {
544
+ return { error: `Claude 兼容代理仅支持上游 responses provider: ${providerName}` };
545
+ }
546
+
547
+ const baseUrl = typeof provider.base_url === 'string' ? provider.base_url.trim() : '';
548
+ if (!baseUrl || !isValidHttpUrl(baseUrl)) {
549
+ return { error: `上游 provider base_url 无效: ${providerName}` };
550
+ }
551
+
552
+ let token = '';
553
+ if (settings.authSource === 'profile') {
554
+ token = resolveAuthTokenFromCurrentProfile();
555
+ } else if (settings.authSource === 'provider') {
556
+ token = typeof provider.preferred_auth_method === 'string' ? provider.preferred_auth_method.trim() : '';
557
+ if (!token) {
558
+ token = resolveAuthTokenFromCurrentProfile();
559
+ }
560
+ }
561
+
562
+ let authHeader = '';
563
+ if (token) {
564
+ authHeader = /^bearer\s+/i.test(token) ? token : `Bearer ${token}`;
565
+ }
566
+
567
+ return {
568
+ providerName,
569
+ baseUrl: normalizeBaseUrl(baseUrl),
570
+ authHeader
571
+ };
572
+ }
573
+
574
+ function buildBuiltinClaudeProxyRequestAuthHeader(req, settings, upstream) {
575
+ if (settings && settings.authSource === 'request') {
576
+ const apiKey = typeof req.headers['x-api-key'] === 'string'
577
+ ? req.headers['x-api-key'].trim()
578
+ : '';
579
+ if (!apiKey) {
580
+ return { error: '缺少 x-api-key,无法转发到上游 responses provider', statusCode: 401 };
581
+ }
582
+ return {
583
+ authHeader: /^bearer\s+/i.test(apiKey) ? apiKey : `Bearer ${apiKey}`
584
+ };
585
+ }
586
+ return { authHeader: upstream.authHeader || '' };
587
+ }
588
+
589
+ function readJsonRequestBody(req, options = {}) {
590
+ const maxBytes = Number.isFinite(options.maxBytes) && options.maxBytes > 0
591
+ ? Math.floor(options.maxBytes)
592
+ : MAX_API_BODY_SIZE;
593
+ return new Promise((resolve, reject) => {
594
+ const chunks = [];
595
+ let total = 0;
596
+ req.on('data', (chunk) => {
597
+ total += chunk.length;
598
+ if (total > maxBytes) {
599
+ reject(new Error(`request body too large (${maxBytes} bytes max)`));
600
+ try { req.destroy(); } catch (_) {}
601
+ return;
602
+ }
603
+ chunks.push(chunk);
604
+ });
605
+ req.on('error', reject);
606
+ req.on('end', () => {
607
+ const raw = Buffer.concat(chunks).toString('utf-8').trim();
608
+ if (!raw) {
609
+ resolve({});
610
+ return;
611
+ }
612
+ try {
613
+ resolve(JSON.parse(raw));
614
+ } catch (e) {
615
+ reject(new Error(`invalid JSON body: ${e.message}`));
616
+ }
617
+ });
618
+ });
619
+ }
620
+
621
+ function extractProxyErrorMessage(payload, fallback = '') {
622
+ if (!payload || typeof payload !== 'object') {
623
+ return fallback || 'upstream request failed';
624
+ }
625
+ if (payload.error && typeof payload.error === 'object') {
626
+ if (typeof payload.error.message === 'string' && payload.error.message.trim()) {
627
+ return payload.error.message.trim();
628
+ }
629
+ if (typeof payload.error.error === 'string' && payload.error.error.trim()) {
630
+ return payload.error.error.trim();
631
+ }
632
+ }
633
+ if (typeof payload.message === 'string' && payload.message.trim()) {
634
+ return payload.message.trim();
635
+ }
636
+ if (typeof payload.error === 'string' && payload.error.trim()) {
637
+ return payload.error.trim();
638
+ }
639
+ return fallback || 'upstream request failed';
640
+ }
641
+
642
+ function writeAnthropicProxyError(res, statusCode, message, type = 'api_error') {
643
+ const body = JSON.stringify({
644
+ type: 'error',
645
+ error: {
646
+ type,
647
+ message: typeof message === 'string' && message.trim() ? message.trim() : 'request failed'
648
+ }
649
+ });
650
+ res.writeHead(statusCode, {
651
+ 'Content-Type': 'application/json; charset=utf-8',
652
+ 'Content-Length': Buffer.byteLength(body, 'utf-8')
653
+ });
654
+ res.end(body, 'utf-8');
655
+ }
656
+
657
+ function requestBuiltinClaudeProxyUpstream(upstream, requestOptions = {}) {
658
+ const pathSuffix = typeof requestOptions.pathSuffix === 'string' ? requestOptions.pathSuffix : '';
659
+ const targetBase = joinApiUrl(upstream.baseUrl, pathSuffix);
660
+ if (!targetBase) {
661
+ return Promise.reject(new Error('failed to build upstream URL'));
662
+ }
663
+
664
+ let targetUrl;
665
+ try {
666
+ targetUrl = new URL(targetBase);
667
+ } catch (e) {
668
+ return Promise.reject(new Error(`invalid upstream URL: ${e.message}`));
669
+ }
670
+
671
+ const bodyText = requestOptions.body === undefined ? '' : JSON.stringify(requestOptions.body);
672
+ const headers = {
673
+ Accept: 'application/json',
674
+ ...(isPlainObject(requestOptions.headers) ? requestOptions.headers : {})
675
+ };
676
+ if (bodyText) {
677
+ headers['Content-Type'] = 'application/json';
678
+ headers['Content-Length'] = Buffer.byteLength(bodyText);
679
+ }
680
+ if (requestOptions.authHeader) {
681
+ headers.authorization = requestOptions.authHeader;
682
+ }
683
+ headers['x-codexmate-claude-proxy'] = '1';
684
+
685
+ const transport = targetUrl.protocol === 'https:' ? https : http;
686
+ const timeoutMs = Number.isFinite(requestOptions.timeoutMs) && requestOptions.timeoutMs > 0
687
+ ? requestOptions.timeoutMs
688
+ : DEFAULT_BUILTIN_CLAUDE_PROXY_SETTINGS.timeoutMs;
689
+
690
+ return new Promise((resolve, reject) => {
691
+ const upstreamReq = transport.request({
692
+ protocol: targetUrl.protocol,
693
+ hostname: targetUrl.hostname,
694
+ port: targetUrl.port || (targetUrl.protocol === 'https:' ? 443 : 80),
695
+ method: requestOptions.method || 'POST',
696
+ path: `${targetUrl.pathname}${targetUrl.search}`,
697
+ headers,
698
+ agent: targetUrl.protocol === 'https:' ? HTTPS_KEEP_ALIVE_AGENT : HTTP_KEEP_ALIVE_AGENT
699
+ }, (upstreamRes) => {
700
+ const chunks = [];
701
+ let total = 0;
702
+ upstreamRes.on('data', (chunk) => {
703
+ total += chunk.length;
704
+ if (total > MAX_API_BODY_SIZE) {
705
+ upstreamReq.destroy(new Error(`upstream body too large (${MAX_API_BODY_SIZE} bytes max)`));
706
+ return;
707
+ }
708
+ chunks.push(chunk);
709
+ });
710
+ upstreamRes.on('error', reject);
711
+ upstreamRes.on('end', () => {
712
+ const rawBody = Buffer.concat(chunks).toString('utf-8');
713
+ let payload = null;
714
+ if (rawBody.trim()) {
715
+ try {
716
+ payload = JSON.parse(rawBody);
717
+ } catch (_) {
718
+ payload = null;
719
+ }
720
+ }
721
+ resolve({
722
+ statusCode: upstreamRes.statusCode || 502,
723
+ headers: upstreamRes.headers,
724
+ rawBody,
725
+ payload
726
+ });
727
+ });
728
+ });
729
+
730
+ upstreamReq.setTimeout(timeoutMs, () => {
731
+ upstreamReq.destroy(new Error(`upstream timeout (${timeoutMs}ms)`));
732
+ });
733
+ upstreamReq.on('error', reject);
734
+ if (bodyText) {
735
+ upstreamReq.write(bodyText, 'utf-8');
736
+ }
737
+ upstreamReq.end();
738
+ });
739
+ }
740
+
741
+ function writeAnthropicStreamEvents(res, message) {
742
+ const events = buildAnthropicStreamEvents(message);
743
+ res.writeHead(200, {
744
+ 'Content-Type': 'text/event-stream; charset=utf-8',
745
+ 'Cache-Control': 'no-cache, no-transform',
746
+ Connection: 'keep-alive',
747
+ 'X-Accel-Buffering': 'no'
748
+ });
749
+ for (const event of events) {
750
+ if (event && event.event) {
751
+ res.write(`event: ${event.event}\n`);
752
+ }
753
+ res.write(`data: ${JSON.stringify(event && event.data ? event.data : {})}\n\n`);
754
+ }
755
+ res.end();
756
+ }
757
+
758
+ async function handleBuiltinClaudeProxyRequest(req, res, settings, upstream) {
759
+ let parsedIncoming;
760
+ try {
761
+ parsedIncoming = new URL(req.url || '/', 'http://localhost');
762
+ } catch (e) {
763
+ writeAnthropicProxyError(res, 400, 'invalid request path', 'invalid_request_error');
764
+ return;
765
+ }
766
+
767
+ const incomingPath = parsedIncoming.pathname || '/';
768
+ if (incomingPath === '/health' || incomingPath === '/status') {
769
+ const body = JSON.stringify({
770
+ ok: true,
771
+ upstreamProvider: upstream.providerName,
772
+ upstreamBaseUrl: upstream.baseUrl,
773
+ mode: 'anthropic-to-responses'
774
+ });
775
+ res.writeHead(200, {
776
+ 'Content-Type': 'application/json; charset=utf-8',
777
+ 'Content-Length': Buffer.byteLength(body, 'utf-8')
778
+ });
779
+ res.end(body, 'utf-8');
780
+ return;
781
+ }
782
+
783
+ const authResult = buildBuiltinClaudeProxyRequestAuthHeader(req, settings, upstream);
784
+ if (authResult.error) {
785
+ writeAnthropicProxyError(res, authResult.statusCode || 401, authResult.error, 'authentication_error');
786
+ return;
787
+ }
788
+
789
+ if (incomingPath === '/v1/models') {
790
+ if ((req.method || 'GET').toUpperCase() !== 'GET') {
791
+ res.writeHead(405, { Allow: 'GET' });
792
+ res.end();
793
+ return;
794
+ }
795
+ const upstreamResponse = await requestBuiltinClaudeProxyUpstream(upstream, {
796
+ method: 'GET',
797
+ pathSuffix: 'models',
798
+ authHeader: authResult.authHeader,
799
+ timeoutMs: settings.timeoutMs
800
+ });
801
+ if (upstreamResponse.statusCode < 200 || upstreamResponse.statusCode >= 300) {
802
+ writeAnthropicProxyError(
803
+ res,
804
+ upstreamResponse.statusCode,
805
+ extractProxyErrorMessage(upstreamResponse.payload, upstreamResponse.rawBody),
806
+ 'api_error'
807
+ );
808
+ return;
809
+ }
810
+ const body = JSON.stringify(buildAnthropicModelsPayload(upstreamResponse.payload));
811
+ res.writeHead(200, {
812
+ 'Content-Type': 'application/json; charset=utf-8',
813
+ 'Content-Length': Buffer.byteLength(body, 'utf-8')
814
+ });
815
+ res.end(body, 'utf-8');
816
+ return;
817
+ }
818
+
819
+ if (incomingPath !== '/v1/messages') {
820
+ writeAnthropicProxyError(res, 404, 'Claude 兼容代理仅支持 /v1/messages 与 /v1/models', 'not_found_error');
821
+ return;
822
+ }
823
+
824
+ if ((req.method || 'POST').toUpperCase() !== 'POST') {
825
+ res.writeHead(405, { Allow: 'POST' });
826
+ res.end();
827
+ return;
828
+ }
829
+
830
+ const payload = await readJsonRequestBody(req);
831
+ const upstreamRequestBody = buildBuiltinClaudeResponsesRequest(payload);
832
+ const upstreamResponse = await requestBuiltinClaudeProxyUpstream(upstream, {
833
+ method: 'POST',
834
+ pathSuffix: 'responses',
835
+ body: upstreamRequestBody,
836
+ authHeader: authResult.authHeader,
837
+ timeoutMs: settings.timeoutMs
838
+ });
839
+
840
+ if (upstreamResponse.statusCode < 200 || upstreamResponse.statusCode >= 300) {
841
+ writeAnthropicProxyError(
842
+ res,
843
+ upstreamResponse.statusCode,
844
+ extractProxyErrorMessage(upstreamResponse.payload, upstreamResponse.rawBody),
845
+ 'api_error'
846
+ );
847
+ return;
848
+ }
849
+
850
+ const anthropicMessage = buildAnthropicMessageFromResponses(upstreamResponse.payload || {}, payload);
851
+ if (payload.stream === true) {
852
+ writeAnthropicStreamEvents(res, anthropicMessage);
853
+ return;
854
+ }
855
+
856
+ const body = JSON.stringify(anthropicMessage);
857
+ res.writeHead(200, {
858
+ 'Content-Type': 'application/json; charset=utf-8',
859
+ 'Content-Length': Buffer.byteLength(body, 'utf-8')
860
+ });
861
+ res.end(body, 'utf-8');
862
+ }
863
+
864
+ function createBuiltinClaudeProxyServer(settings, upstream) {
865
+ const connections = new Set();
866
+ const server = http.createServer((req, res) => {
867
+ const remoteAddr = req && req.socket ? req.socket.remoteAddress : '';
868
+ const isLoopback = !remoteAddr
869
+ || remoteAddr === '127.0.0.1'
870
+ || remoteAddr === '::1'
871
+ || remoteAddr === '::ffff:127.0.0.1';
872
+ if (!isLoopback) {
873
+ const expected = typeof process.env.CODEXMATE_HTTP_TOKEN === 'string'
874
+ ? process.env.CODEXMATE_HTTP_TOKEN.trim()
875
+ : '';
876
+ if (!expected) {
877
+ writeAnthropicProxyError(res, 403, 'Remote access is disabled (set CODEXMATE_HTTP_TOKEN)', 'authentication_error');
878
+ return;
879
+ }
880
+ const headers = req && req.headers && typeof req.headers === 'object' ? req.headers : {};
881
+ const rawAuth = typeof headers.authorization === 'string' ? headers.authorization.trim() : '';
882
+ const match = rawAuth ? rawAuth.match(/^bearer\s+(.+)$/i) : null;
883
+ const actual = match && match[1]
884
+ ? match[1].trim()
885
+ : (rawAuth ? rawAuth : (typeof headers['x-codexmate-token'] === 'string' ? String(headers['x-codexmate-token']).trim() : ''));
886
+ if (!actual || actual !== expected) {
887
+ writeAnthropicProxyError(res, 401, 'Unauthorized', 'authentication_error');
888
+ return;
889
+ }
890
+ }
891
+ handleBuiltinClaudeProxyRequest(req, res, settings, upstream).catch((err) => {
892
+ if (res.headersSent) {
893
+ try { res.destroy(err); } catch (_) {}
894
+ return;
895
+ }
896
+ writeAnthropicProxyError(res, 502, `claude proxy request failed: ${err.message}`, 'api_error');
897
+ });
898
+ });
899
+
900
+ server.on('connection', (socket) => {
901
+ connections.add(socket);
902
+ socket.on('close', () => connections.delete(socket));
903
+ });
904
+
905
+ return new Promise((resolve, reject) => {
906
+ server.once('error', reject);
907
+ server.listen(settings.port, settings.host, () => {
908
+ server.removeListener('error', reject);
909
+ resolve({
910
+ server,
911
+ connections,
912
+ settings,
913
+ upstream,
914
+ startedAt: toIsoTime(Date.now()),
915
+ listenUrl: buildBuiltinClaudeProxyListenUrl(settings)
916
+ });
917
+ });
918
+ });
919
+ }
920
+
921
+ async function startBuiltinClaudeProxyRuntime(payload = {}) {
922
+ if (runtime) {
923
+ return {
924
+ error: 'Claude 兼容代理已在运行',
925
+ runtime: {
926
+ listenUrl: runtime.listenUrl,
927
+ upstreamProvider: runtime.upstream.providerName
928
+ }
929
+ };
930
+ }
931
+
932
+ const saveResult = saveBuiltinClaudeProxySettings(payload);
933
+ if (saveResult.error) {
934
+ return { error: saveResult.error };
935
+ }
936
+ const settings = saveResult.settings;
937
+ const upstream = resolveBuiltinClaudeProxyUpstream(settings);
938
+ if (upstream.error) {
939
+ return { error: upstream.error };
940
+ }
941
+
942
+ try {
943
+ runtime = await createBuiltinClaudeProxyServer(settings, upstream);
944
+ return {
945
+ success: true,
946
+ running: true,
947
+ listenUrl: runtime.listenUrl,
948
+ upstreamProvider: upstream.providerName,
949
+ mode: 'anthropic-to-responses',
950
+ settings
951
+ };
952
+ } catch (e) {
953
+ return { error: `启动 Claude 兼容代理失败: ${e.message}` };
954
+ }
955
+ }
956
+
957
+ async function stopBuiltinClaudeProxyRuntime() {
958
+ if (!runtime) {
959
+ return { success: true, running: false };
960
+ }
961
+ const currentRuntime = runtime;
962
+ runtime = null;
963
+
964
+ await new Promise((resolve) => {
965
+ let settled = false;
966
+ const finish = () => {
967
+ if (settled) return;
968
+ settled = true;
969
+ resolve();
970
+ };
971
+
972
+ currentRuntime.server.close(() => finish());
973
+ setTimeout(() => finish(), 1000);
974
+ });
975
+
976
+ for (const socket of currentRuntime.connections) {
977
+ try { socket.destroy(); } catch (_) {}
978
+ }
979
+ currentRuntime.connections.clear();
980
+
981
+ return {
982
+ success: true,
983
+ running: false
984
+ };
985
+ }
986
+
987
+ function getBuiltinClaudeProxyStatus() {
988
+ const settings = readBuiltinClaudeProxySettings();
989
+ return {
990
+ running: !!runtime,
991
+ settings,
992
+ runtime: runtime
993
+ ? {
994
+ startedAt: runtime.startedAt,
995
+ listenUrl: runtime.listenUrl,
996
+ upstreamProvider: runtime.upstream.providerName,
997
+ upstreamBaseUrl: runtime.upstream.baseUrl,
998
+ mode: 'anthropic-to-responses'
999
+ }
1000
+ : null
1001
+ };
1002
+ }
1003
+
1004
+ return {
1005
+ normalizeBuiltinClaudeProxySettings,
1006
+ readBuiltinClaudeProxySettings,
1007
+ saveBuiltinClaudeProxySettings,
1008
+ buildBuiltinClaudeProxyListenUrl,
1009
+ resolveBuiltinClaudeProxyUpstream,
1010
+ startBuiltinClaudeProxyRuntime,
1011
+ stopBuiltinClaudeProxyRuntime,
1012
+ getBuiltinClaudeProxyStatus
1013
+ };
1014
+ }
1015
+
1016
+ module.exports = {
1017
+ createBuiltinClaudeProxyRuntimeController,
1018
+ buildBuiltinClaudeResponsesRequest,
1019
+ buildAnthropicMessageFromResponses,
1020
+ buildAnthropicStreamEvents,
1021
+ buildAnthropicModelsPayload
1022
+ };