codexmate 0.0.29 → 0.0.31

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 (145) hide show
  1. package/README.md +363 -421
  2. package/README.zh.md +371 -354
  3. package/cli/agents-files.js +224 -224
  4. package/cli/archive-helpers.js +446 -446
  5. package/cli/auth-profiles.js +375 -375
  6. package/cli/builtin-proxy.js +1725 -1725
  7. package/cli/claude-proxy.js +1022 -1022
  8. package/cli/config-bootstrap.js +402 -384
  9. package/cli/config-health.js +454 -338
  10. package/cli/doctor-core.js +903 -903
  11. package/cli/import-skills-url.js +356 -356
  12. package/cli/local-bridge.js +324 -0
  13. package/cli/openai-bridge.js +1653 -1576
  14. package/cli/openclaw-config.js +629 -629
  15. package/cli/session-convert-args.js +69 -65
  16. package/cli/session-convert-io.js +82 -82
  17. package/cli/session-convert.js +150 -43
  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/zip-commands.js +510 -510
  23. package/cli.js +15778 -15340
  24. package/lib/automation.js +404 -404
  25. package/lib/cli-file-utils.js +151 -151
  26. package/lib/cli-models-utils.js +440 -440
  27. package/lib/cli-network-utils.js +190 -190
  28. package/lib/cli-path-utils.js +85 -85
  29. package/lib/cli-session-utils.js +121 -121
  30. package/lib/cli-sessions.js +417 -417
  31. package/lib/cli-utils.js +155 -155
  32. package/lib/cli-webhook.js +126 -126
  33. package/lib/download-artifacts.js +92 -92
  34. package/lib/mcp-stdio.js +453 -453
  35. package/lib/task-orchestrator.js +869 -869
  36. package/lib/text-diff.js +303 -303
  37. package/lib/workflow-engine.js +340 -340
  38. package/package.json +76 -76
  39. package/plugins/README.md +20 -20
  40. package/plugins/README.zh-CN.md +20 -20
  41. package/plugins/prompt-templates/comment-polish/index.mjs +25 -25
  42. package/plugins/prompt-templates/computed.mjs +253 -253
  43. package/plugins/prompt-templates/index.mjs +8 -8
  44. package/plugins/prompt-templates/manifest.mjs +15 -15
  45. package/plugins/prompt-templates/methods.mjs +553 -553
  46. package/plugins/prompt-templates/overview.mjs +91 -91
  47. package/plugins/prompt-templates/ownership.mjs +19 -19
  48. package/plugins/prompt-templates/rule-ack/index.mjs +21 -21
  49. package/plugins/prompt-templates/storage.mjs +64 -64
  50. package/plugins/registry.mjs +16 -16
  51. package/web-ui/app.js +646 -639
  52. package/web-ui/index.html +36 -36
  53. package/web-ui/logic.agents-diff.mjs +386 -386
  54. package/web-ui/logic.claude.mjs +168 -168
  55. package/web-ui/logic.codex.mjs +69 -69
  56. package/web-ui/logic.mjs +5 -5
  57. package/web-ui/logic.runtime.mjs +128 -128
  58. package/web-ui/logic.session-convert.mjs +70 -70
  59. package/web-ui/logic.sessions.mjs +765 -765
  60. package/web-ui/modules/api.mjs +90 -90
  61. package/web-ui/modules/app.computed.dashboard.mjs +248 -225
  62. package/web-ui/modules/app.computed.index.mjs +17 -17
  63. package/web-ui/modules/app.computed.main-tabs.mjs +205 -205
  64. package/web-ui/modules/app.computed.session.mjs +999 -999
  65. package/web-ui/modules/app.constants.mjs +15 -15
  66. package/web-ui/modules/app.methods.agents.mjs +632 -632
  67. package/web-ui/modules/app.methods.claude-config.mjs +200 -200
  68. package/web-ui/modules/app.methods.codex-config.mjs +841 -892
  69. package/web-ui/modules/app.methods.index.mjs +94 -94
  70. package/web-ui/modules/app.methods.install.mjs +205 -205
  71. package/web-ui/modules/app.methods.navigation.mjs +774 -761
  72. package/web-ui/modules/app.methods.openclaw-core.mjs +814 -814
  73. package/web-ui/modules/app.methods.openclaw-editing.mjs +372 -372
  74. package/web-ui/modules/app.methods.openclaw-persist.mjs +369 -369
  75. package/web-ui/modules/app.methods.providers.mjs +529 -493
  76. package/web-ui/modules/app.methods.runtime.mjs +345 -345
  77. package/web-ui/modules/app.methods.session-actions.mjs +591 -593
  78. package/web-ui/modules/app.methods.session-browser.mjs +984 -984
  79. package/web-ui/modules/app.methods.session-timeline.mjs +479 -479
  80. package/web-ui/modules/app.methods.session-trash.mjs +438 -438
  81. package/web-ui/modules/app.methods.startup-claude.mjs +534 -533
  82. package/web-ui/modules/app.methods.task-orchestration.mjs +556 -556
  83. package/web-ui/modules/app.methods.webhook.mjs +79 -79
  84. package/web-ui/modules/config-mode.computed.mjs +124 -124
  85. package/web-ui/modules/config-template-confirm-pref.mjs +33 -33
  86. package/web-ui/modules/i18n.dict.mjs +3174 -3131
  87. package/web-ui/modules/i18n.mjs +62 -62
  88. package/web-ui/modules/plugins.computed.mjs +3 -3
  89. package/web-ui/modules/plugins.methods.mjs +3 -3
  90. package/web-ui/modules/plugins.storage.mjs +11 -11
  91. package/web-ui/modules/provider-url-display.mjs +17 -17
  92. package/web-ui/modules/sessions-filters-url.mjs +85 -85
  93. package/web-ui/modules/skills.computed.mjs +107 -107
  94. package/web-ui/modules/skills.methods.mjs +482 -481
  95. package/web-ui/partials/index/layout-footer.html +13 -13
  96. package/web-ui/partials/index/layout-header.html +500 -500
  97. package/web-ui/partials/index/modal-config-template-agents.html +174 -174
  98. package/web-ui/partials/index/modal-confirm-toast.html +32 -32
  99. package/web-ui/partials/index/modal-health-check.html +45 -45
  100. package/web-ui/partials/index/modal-openclaw-config.html +280 -280
  101. package/web-ui/partials/index/modal-skills.html +200 -200
  102. package/web-ui/partials/index/modals-basic.html +162 -162
  103. package/web-ui/partials/index/panel-config-claude.html +194 -194
  104. package/web-ui/partials/index/panel-config-codex.html +357 -323
  105. package/web-ui/partials/index/panel-config-codex.html.bak +337 -0
  106. package/web-ui/partials/index/panel-config-openclaw.html +83 -83
  107. package/web-ui/partials/index/panel-dashboard.html +219 -186
  108. package/web-ui/partials/index/panel-docs.html +147 -147
  109. package/web-ui/partials/index/panel-market.html +177 -177
  110. package/web-ui/partials/index/panel-orchestration.html +391 -391
  111. package/web-ui/partials/index/panel-plugins.html +253 -253
  112. package/web-ui/partials/index/panel-sessions.html +302 -316
  113. package/web-ui/partials/index/panel-settings.html +190 -190
  114. package/web-ui/partials/index/panel-trash.html +88 -88
  115. package/web-ui/partials/index/panel-usage.html +371 -371
  116. package/web-ui/res/json5.min.js +1 -1
  117. package/web-ui/res/vue.global.prod.js +13 -13
  118. package/web-ui/session-helpers.mjs +591 -576
  119. package/web-ui/source-bundle.cjs +233 -233
  120. package/web-ui/styles/base-theme.css +281 -281
  121. package/web-ui/styles/bridge-pool.css +197 -0
  122. package/web-ui/styles/controls-forms.css +422 -422
  123. package/web-ui/styles/dashboard.css +406 -274
  124. package/web-ui/styles/docs-panel.css +271 -271
  125. package/web-ui/styles/feedback.css +108 -108
  126. package/web-ui/styles/health-check-dialog.css +144 -144
  127. package/web-ui/styles/layout-shell.css +626 -626
  128. package/web-ui/styles/modals-core.css +466 -466
  129. package/web-ui/styles/navigation-panels.css +391 -391
  130. package/web-ui/styles/openclaw-structured.css +266 -266
  131. package/web-ui/styles/plugins-panel.css +564 -523
  132. package/web-ui/styles/responsive.css +454 -454
  133. package/web-ui/styles/sessions-list.css +417 -419
  134. package/web-ui/styles/sessions-preview.css +407 -411
  135. package/web-ui/styles/sessions-toolbar-trash.css +348 -330
  136. package/web-ui/styles/sessions-usage.css +1040 -1040
  137. package/web-ui/styles/settings-panel.css +349 -349
  138. package/web-ui/styles/skills-list.css +305 -303
  139. package/web-ui/styles/skills-market.css +429 -406
  140. package/web-ui/styles/task-orchestration.css +822 -822
  141. package/web-ui/styles/titles-cards.css +472 -472
  142. package/web-ui/styles/trash-panel.css +90 -90
  143. package/web-ui/styles/webhook.css +81 -81
  144. package/web-ui/styles.css +24 -23
  145. 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
+ };