codexmate 0.0.27 → 0.0.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/README.md +421 -421
  2. package/README.zh.md +354 -354
  3. package/cli/agents-files.js +224 -224
  4. package/cli/archive-helpers.js +446 -446
  5. package/cli/auth-profiles.js +375 -375
  6. package/cli/builtin-proxy.js +1620 -1299
  7. package/cli/claude-proxy.js +1022 -1022
  8. package/cli/config-bootstrap.js +384 -384
  9. package/cli/config-health.js +338 -338
  10. package/cli/doctor-core.js +903 -903
  11. package/cli/import-skills-url.js +356 -356
  12. package/cli/openai-bridge.js +1489 -1091
  13. package/cli/openclaw-config.js +629 -629
  14. package/cli/session-convert-args.js +65 -65
  15. package/cli/session-convert-io.js +82 -82
  16. package/cli/session-convert.js +43 -43
  17. package/cli/session-usage.concurrent.js +28 -28
  18. package/cli/session-usage.js +118 -118
  19. package/cli/session-usage.models.js +176 -176
  20. package/cli/skills.js +1141 -1141
  21. package/cli/zip-commands.js +510 -510
  22. package/cli.js +15264 -15251
  23. package/lib/automation.js +404 -404
  24. package/lib/cli-file-utils.js +151 -151
  25. package/lib/cli-models-utils.js +440 -379
  26. package/lib/cli-network-utils.js +190 -190
  27. package/lib/cli-path-utils.js +85 -85
  28. package/lib/cli-session-utils.js +121 -121
  29. package/lib/cli-sessions.js +417 -417
  30. package/lib/cli-utils.js +155 -155
  31. package/lib/download-artifacts.js +92 -92
  32. package/lib/mcp-stdio.js +453 -453
  33. package/lib/task-orchestrator.js +869 -869
  34. package/lib/text-diff.js +303 -303
  35. package/lib/workflow-engine.js +340 -340
  36. package/package.json +3 -1
  37. package/plugins/README.md +20 -20
  38. package/plugins/README.zh-CN.md +20 -20
  39. package/plugins/prompt-templates/comment-polish/index.mjs +25 -25
  40. package/plugins/prompt-templates/computed.mjs +253 -253
  41. package/plugins/prompt-templates/index.mjs +8 -8
  42. package/plugins/prompt-templates/manifest.mjs +15 -15
  43. package/plugins/prompt-templates/methods.mjs +553 -619
  44. package/plugins/prompt-templates/overview.mjs +91 -90
  45. package/plugins/prompt-templates/ownership.mjs +19 -19
  46. package/plugins/prompt-templates/rule-ack/index.mjs +21 -21
  47. package/plugins/prompt-templates/storage.mjs +64 -64
  48. package/plugins/registry.mjs +16 -16
  49. package/web-ui/app.js +625 -634
  50. package/web-ui/index.html +35 -35
  51. package/web-ui/logic.agents-diff.mjs +386 -386
  52. package/web-ui/logic.claude.mjs +168 -168
  53. package/web-ui/logic.codex.mjs +56 -0
  54. package/web-ui/logic.mjs +5 -5
  55. package/web-ui/logic.runtime.mjs +128 -128
  56. package/web-ui/logic.session-convert.mjs +70 -70
  57. package/web-ui/logic.sessions.mjs +765 -765
  58. package/web-ui/modules/api.mjs +90 -90
  59. package/web-ui/modules/app.computed.dashboard.mjs +225 -171
  60. package/web-ui/modules/app.computed.index.mjs +17 -17
  61. package/web-ui/modules/app.computed.main-tabs.mjs +205 -205
  62. package/web-ui/modules/app.computed.session.mjs +994 -994
  63. package/web-ui/modules/app.constants.mjs +15 -15
  64. package/web-ui/modules/app.methods.agents.mjs +632 -632
  65. package/web-ui/modules/app.methods.claude-config.mjs +190 -184
  66. package/web-ui/modules/app.methods.codex-config.mjs +892 -860
  67. package/web-ui/modules/app.methods.index.mjs +92 -92
  68. package/web-ui/modules/app.methods.install.mjs +205 -205
  69. package/web-ui/modules/app.methods.navigation.mjs +743 -743
  70. package/web-ui/modules/app.methods.openclaw-core.mjs +814 -814
  71. package/web-ui/modules/app.methods.openclaw-editing.mjs +372 -372
  72. package/web-ui/modules/app.methods.openclaw-persist.mjs +369 -369
  73. package/web-ui/modules/app.methods.providers.mjs +412 -404
  74. package/web-ui/modules/app.methods.runtime.mjs +345 -345
  75. package/web-ui/modules/app.methods.session-actions.mjs +593 -596
  76. package/web-ui/modules/app.methods.session-browser.mjs +984 -989
  77. package/web-ui/modules/app.methods.session-timeline.mjs +479 -479
  78. package/web-ui/modules/app.methods.session-trash.mjs +439 -439
  79. package/web-ui/modules/app.methods.startup-claude.mjs +533 -526
  80. package/web-ui/modules/app.methods.task-orchestration.mjs +556 -556
  81. package/web-ui/modules/config-mode.computed.mjs +124 -124
  82. package/web-ui/modules/config-template-confirm-pref.mjs +33 -33
  83. package/web-ui/modules/i18n.dict.mjs +2109 -2131
  84. package/web-ui/modules/i18n.mjs +56 -56
  85. package/web-ui/modules/plugins.computed.mjs +3 -3
  86. package/web-ui/modules/plugins.methods.mjs +3 -3
  87. package/web-ui/modules/plugins.storage.mjs +11 -11
  88. package/web-ui/modules/provider-url-display.mjs +17 -0
  89. package/web-ui/modules/sessions-filters-url.mjs +85 -85
  90. package/web-ui/modules/skills.computed.mjs +107 -107
  91. package/web-ui/modules/skills.methods.mjs +481 -481
  92. package/web-ui/partials/index/layout-footer.html +13 -13
  93. package/web-ui/partials/index/layout-header.html +475 -475
  94. package/web-ui/partials/index/modal-config-template-agents.html +174 -174
  95. package/web-ui/partials/index/modal-confirm-toast.html +32 -32
  96. package/web-ui/partials/index/modal-health-check.html +45 -45
  97. package/web-ui/partials/index/modal-openclaw-config.html +280 -280
  98. package/web-ui/partials/index/modal-skills.html +200 -200
  99. package/web-ui/partials/index/modals-basic.html +165 -165
  100. package/web-ui/partials/index/panel-config-claude.html +188 -187
  101. package/web-ui/partials/index/panel-config-codex.html +312 -283
  102. package/web-ui/partials/index/panel-config-openclaw.html +83 -83
  103. package/web-ui/partials/index/panel-dashboard.html +186 -186
  104. package/web-ui/partials/index/panel-docs.html +147 -147
  105. package/web-ui/partials/index/panel-market.html +177 -177
  106. package/web-ui/partials/index/panel-orchestration.html +391 -391
  107. package/web-ui/partials/index/panel-plugins.html +253 -279
  108. package/web-ui/partials/index/panel-sessions.html +316 -326
  109. package/web-ui/partials/index/panel-settings.html +253 -274
  110. package/web-ui/partials/index/panel-usage.html +371 -371
  111. package/web-ui/res/json5.min.js +1 -1
  112. package/web-ui/res/vue.global.prod.js +13 -13
  113. package/web-ui/session-helpers.mjs +576 -576
  114. package/web-ui/source-bundle.cjs +233 -233
  115. package/web-ui/styles/base-theme.css +281 -268
  116. package/web-ui/styles/controls-forms.css +422 -423
  117. package/web-ui/styles/dashboard.css +274 -274
  118. package/web-ui/styles/docs-panel.css +247 -247
  119. package/web-ui/styles/feedback.css +108 -108
  120. package/web-ui/styles/health-check-dialog.css +144 -144
  121. package/web-ui/styles/layout-shell.css +606 -603
  122. package/web-ui/styles/modals-core.css +466 -464
  123. package/web-ui/styles/navigation-panels.css +391 -390
  124. package/web-ui/styles/openclaw-structured.css +266 -266
  125. package/web-ui/styles/plugins-panel.css +523 -523
  126. package/web-ui/styles/responsive.css +454 -454
  127. package/web-ui/styles/sessions-list.css +419 -415
  128. package/web-ui/styles/sessions-preview.css +411 -411
  129. package/web-ui/styles/sessions-toolbar-trash.css +330 -330
  130. package/web-ui/styles/sessions-usage.css +1040 -1040
  131. package/web-ui/styles/settings-panel.css +185 -185
  132. package/web-ui/styles/skills-list.css +303 -303
  133. package/web-ui/styles/skills-market.css +406 -406
  134. package/web-ui/styles/task-orchestration.css +822 -822
  135. package/web-ui/styles/titles-cards.css +472 -408
  136. package/web-ui/styles.css +21 -21
  137. package/web-ui.html +17 -17
@@ -1,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
+ };