codexmate 0.0.25 → 0.0.26

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 (135) hide show
  1. package/README.md +416 -413
  2. package/README.zh.md +349 -346
  3. package/cli/agents-files.js +224 -224
  4. package/cli/archive-helpers.js +446 -446
  5. package/cli/auth-profiles.js +375 -375
  6. package/cli/builtin-proxy.js +1079 -1079
  7. package/cli/claude-proxy.js +1022 -1022
  8. package/cli/config-bootstrap.js +384 -384
  9. package/cli/config-health.js +338 -338
  10. package/cli/doctor-core.js +903 -903
  11. package/cli/import-skills-url.js +356 -356
  12. package/cli/openai-bridge.js +997 -997
  13. package/cli/openclaw-config.js +629 -629
  14. package/cli/session-convert-args.js +65 -0
  15. package/cli/session-convert-io.js +82 -0
  16. package/cli/session-convert.js +43 -0
  17. package/cli/session-usage.concurrent.js +28 -28
  18. package/cli/session-usage.js +118 -118
  19. package/cli/session-usage.models.js +176 -176
  20. package/cli/skills.js +1141 -1141
  21. package/cli/zip-commands.js +510 -510
  22. package/cli.js +15218 -14736
  23. package/lib/automation.js +404 -404
  24. package/lib/cli-file-utils.js +151 -151
  25. package/lib/cli-models-utils.js +379 -379
  26. package/lib/cli-network-utils.js +190 -190
  27. package/lib/cli-path-utils.js +85 -85
  28. package/lib/cli-session-utils.js +121 -121
  29. package/lib/cli-sessions.js +417 -417
  30. package/lib/cli-utils.js +155 -155
  31. package/lib/download-artifacts.js +92 -92
  32. package/lib/mcp-stdio.js +453 -453
  33. package/lib/task-orchestrator.js +869 -869
  34. package/lib/text-diff.js +303 -303
  35. package/lib/workflow-engine.js +340 -340
  36. package/package.json +74 -74
  37. package/plugins/README.md +20 -20
  38. package/plugins/README.zh-CN.md +20 -20
  39. package/plugins/prompt-templates/comment-polish/index.mjs +25 -25
  40. package/plugins/prompt-templates/computed.mjs +253 -253
  41. package/plugins/prompt-templates/index.mjs +8 -8
  42. package/plugins/prompt-templates/manifest.mjs +15 -15
  43. package/plugins/prompt-templates/methods.mjs +619 -619
  44. package/plugins/prompt-templates/overview.mjs +90 -90
  45. package/plugins/prompt-templates/ownership.mjs +19 -19
  46. package/plugins/prompt-templates/rule-ack/index.mjs +21 -21
  47. package/plugins/prompt-templates/storage.mjs +64 -64
  48. package/plugins/registry.mjs +16 -16
  49. package/web-ui/app.js +625 -612
  50. package/web-ui/index.html +35 -35
  51. package/web-ui/logic.agents-diff.mjs +386 -386
  52. package/web-ui/logic.claude.mjs +168 -168
  53. package/web-ui/logic.mjs +5 -5
  54. package/web-ui/logic.runtime.mjs +128 -128
  55. package/web-ui/logic.session-convert.mjs +70 -0
  56. package/web-ui/logic.sessions.mjs +709 -614
  57. package/web-ui/modules/api.mjs +90 -90
  58. package/web-ui/modules/app.computed.dashboard.mjs +171 -128
  59. package/web-ui/modules/app.computed.index.mjs +17 -17
  60. package/web-ui/modules/app.computed.main-tabs.mjs +205 -205
  61. package/web-ui/modules/app.computed.session.mjs +946 -670
  62. package/web-ui/modules/app.constants.mjs +15 -15
  63. package/web-ui/modules/app.methods.agents.mjs +632 -632
  64. package/web-ui/modules/app.methods.claude-config.mjs +179 -174
  65. package/web-ui/modules/app.methods.codex-config.mjs +860 -784
  66. package/web-ui/modules/app.methods.index.mjs +92 -92
  67. package/web-ui/modules/app.methods.install.mjs +205 -205
  68. package/web-ui/modules/app.methods.navigation.mjs +743 -695
  69. package/web-ui/modules/app.methods.openclaw-core.mjs +814 -814
  70. package/web-ui/modules/app.methods.openclaw-editing.mjs +372 -372
  71. package/web-ui/modules/app.methods.openclaw-persist.mjs +369 -369
  72. package/web-ui/modules/app.methods.providers.mjs +404 -404
  73. package/web-ui/modules/app.methods.runtime.mjs +345 -345
  74. package/web-ui/modules/app.methods.session-actions.mjs +596 -544
  75. package/web-ui/modules/app.methods.session-browser.mjs +985 -722
  76. package/web-ui/modules/app.methods.session-timeline.mjs +479 -448
  77. package/web-ui/modules/app.methods.session-trash.mjs +424 -424
  78. package/web-ui/modules/app.methods.startup-claude.mjs +522 -417
  79. package/web-ui/modules/app.methods.task-orchestration.mjs +556 -556
  80. package/web-ui/modules/config-mode.computed.mjs +124 -124
  81. package/web-ui/modules/config-template-confirm-pref.mjs +33 -33
  82. package/web-ui/modules/i18n.dict.mjs +2113 -2055
  83. package/web-ui/modules/i18n.mjs +56 -56
  84. package/web-ui/modules/plugins.computed.mjs +3 -3
  85. package/web-ui/modules/plugins.methods.mjs +3 -3
  86. package/web-ui/modules/plugins.storage.mjs +11 -11
  87. package/web-ui/modules/sessions-filters-url.mjs +85 -85
  88. package/web-ui/modules/skills.computed.mjs +107 -107
  89. package/web-ui/modules/skills.methods.mjs +481 -481
  90. package/web-ui/partials/index/layout-footer.html +13 -13
  91. package/web-ui/partials/index/layout-header.html +475 -475
  92. package/web-ui/partials/index/modal-config-template-agents.html +174 -174
  93. package/web-ui/partials/index/modal-confirm-toast.html +32 -32
  94. package/web-ui/partials/index/modal-health-check.html +45 -45
  95. package/web-ui/partials/index/modal-openclaw-config.html +280 -280
  96. package/web-ui/partials/index/modal-skills.html +200 -200
  97. package/web-ui/partials/index/modals-basic.html +165 -165
  98. package/web-ui/partials/index/panel-config-claude.html +184 -179
  99. package/web-ui/partials/index/panel-config-codex.html +283 -283
  100. package/web-ui/partials/index/panel-config-openclaw.html +83 -83
  101. package/web-ui/partials/index/panel-dashboard.html +186 -186
  102. package/web-ui/partials/index/panel-docs.html +147 -147
  103. package/web-ui/partials/index/panel-market.html +177 -177
  104. package/web-ui/partials/index/panel-orchestration.html +391 -391
  105. package/web-ui/partials/index/panel-plugins.html +279 -279
  106. package/web-ui/partials/index/panel-sessions.html +326 -303
  107. package/web-ui/partials/index/panel-settings.html +258 -258
  108. package/web-ui/partials/index/panel-usage.html +342 -361
  109. package/web-ui/res/json5.min.js +1 -1
  110. package/web-ui/res/vue.global.prod.js +13 -13
  111. package/web-ui/session-helpers.mjs +576 -573
  112. package/web-ui/source-bundle.cjs +233 -233
  113. package/web-ui/styles/base-theme.css +268 -264
  114. package/web-ui/styles/controls-forms.css +423 -423
  115. package/web-ui/styles/dashboard.css +274 -274
  116. package/web-ui/styles/docs-panel.css +247 -247
  117. package/web-ui/styles/feedback.css +108 -108
  118. package/web-ui/styles/health-check-dialog.css +144 -144
  119. package/web-ui/styles/layout-shell.css +603 -603
  120. package/web-ui/styles/modals-core.css +464 -464
  121. package/web-ui/styles/navigation-panels.css +390 -390
  122. package/web-ui/styles/openclaw-structured.css +266 -266
  123. package/web-ui/styles/plugins-panel.css +523 -523
  124. package/web-ui/styles/responsive.css +454 -454
  125. package/web-ui/styles/sessions-list.css +415 -398
  126. package/web-ui/styles/sessions-preview.css +411 -411
  127. package/web-ui/styles/sessions-toolbar-trash.css +330 -268
  128. package/web-ui/styles/sessions-usage.css +945 -912
  129. package/web-ui/styles/settings-panel.css +166 -166
  130. package/web-ui/styles/skills-list.css +303 -303
  131. package/web-ui/styles/skills-market.css +406 -406
  132. package/web-ui/styles/task-orchestration.css +822 -822
  133. package/web-ui/styles/titles-cards.css +408 -408
  134. package/web-ui/styles.css +21 -21
  135. 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
+ };