aicodeswitch 5.1.1 → 5.1.2

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.
@@ -38,6 +38,7 @@ exports.CODEX_CONFIG_MANAGED_FIELDS = [
38
38
  { path: ['requires_openai_auth'] },
39
39
  { path: ['enableRouteSelection'] },
40
40
  { path: ['model_providers', 'aicodeswitch'], isSection: true },
41
+ { path: ['mcp_servers'], isSection: true, optional: true },
41
42
  ];
42
43
  /**
43
44
  * Codex auth.json 管理字段定义
@@ -0,0 +1,138 @@
1
+ "use strict";
2
+ /**
3
+ * Request body sanitizer for AICodeSwitch proxy.
4
+ *
5
+ * Defensively cleans incoming request bodies before format transformation
6
+ * and upstream forwarding. Catches issues that originate from client bugs
7
+ * (e.g. Codex sending improperly escaped content) so that upstream APIs
8
+ * receive well-formed JSON.
9
+ *
10
+ * Sanitization steps:
11
+ * 1. Strip illegal C0 control characters from string values
12
+ * 2. Fix `function_call.arguments` that are not valid JSON strings
13
+ * 3. Remove `undefined` values from the object tree
14
+ * 4. Guard against circular references and excessive depth
15
+ */
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.sanitizeRequestBody = sanitizeRequestBody;
18
+ // C0 control characters except TAB (0x09), LF (0x0A), CR (0x0D).
19
+ // These are the only three control chars allowed in JSON strings (RFC 8259 §7).
20
+ const CONTROL_CHAR_REGEX = /[\x00-\x08\x0b\x0c\x0e-\x1f]/g;
21
+ const MAX_DEPTH = 64;
22
+ /**
23
+ * Deep-sanitize a request body object.
24
+ *
25
+ * @param body - The parsed request body (a plain JS object).
26
+ * @returns A new object with fixes applied and a list of human-readable
27
+ * change descriptions (empty when nothing was modified).
28
+ */
29
+ function sanitizeRequestBody(body) {
30
+ if (body === null || body === undefined || typeof body !== 'object') {
31
+ return { body, changes: [] };
32
+ }
33
+ const changes = [];
34
+ const seen = new WeakSet();
35
+ const result = sanitizeValue(body, '', changes, seen, 0);
36
+ return { body: result, changes };
37
+ }
38
+ // ---------------------------------------------------------------------------
39
+ // Internal helpers
40
+ // ---------------------------------------------------------------------------
41
+ function sanitizeValue(value, path, changes, seen, depth) {
42
+ // Primitive types ----------------------------------------------------------
43
+ if (value === null)
44
+ return null;
45
+ if (value === undefined) {
46
+ changes.push(`removed undefined at ${path || '$'}`);
47
+ return null; // replaced with null rather than silently dropped
48
+ }
49
+ if (typeof value === 'string') {
50
+ return sanitizeString(value, path, changes);
51
+ }
52
+ if (typeof value !== 'object') {
53
+ return value; // numbers, booleans — pass through
54
+ }
55
+ // Guard: depth -------------------------------------------------------------
56
+ if (depth >= MAX_DEPTH) {
57
+ changes.push(`max depth exceeded at ${path || '$'}`);
58
+ return value;
59
+ }
60
+ // Guard: circular reference ------------------------------------------------
61
+ if (seen.has(value)) {
62
+ changes.push(`circular reference at ${path || '$'}`);
63
+ return '[Circular]';
64
+ }
65
+ seen.add(value);
66
+ // Arrays -------------------------------------------------------------------
67
+ if (Array.isArray(value)) {
68
+ return value.map((item, i) => {
69
+ const itemPath = `${path}[${i}]`;
70
+ const sanitized = sanitizeValue(item, itemPath, changes, seen, depth + 1);
71
+ // Fix function_call.arguments inside Responses API input arrays
72
+ if (sanitized !== null &&
73
+ typeof sanitized === 'object' &&
74
+ !Array.isArray(sanitized) &&
75
+ sanitized.type === 'function_call') {
76
+ fixFunctionCallArguments(sanitized, itemPath, changes);
77
+ }
78
+ return sanitized;
79
+ });
80
+ }
81
+ // Plain objects ------------------------------------------------------------
82
+ const result = {};
83
+ for (const [key, val] of Object.entries(value)) {
84
+ // Remove undefined values entirely
85
+ if (val === undefined) {
86
+ changes.push(`removed undefined key ${path}.${key}`);
87
+ continue;
88
+ }
89
+ const childPath = path ? `${path}.${key}` : key;
90
+ result[key] = sanitizeValue(val, childPath, changes, seen, depth + 1);
91
+ }
92
+ // Post-process: fix function_call.arguments in input arrays
93
+ if (Array.isArray(result.input)) {
94
+ for (let i = 0; i < result.input.length; i++) {
95
+ const item = result.input[i];
96
+ if (item !== null &&
97
+ typeof item === 'object' &&
98
+ !Array.isArray(item) &&
99
+ item.type === 'function_call') {
100
+ fixFunctionCallArguments(item, `${path}.input[${i}]`, changes);
101
+ }
102
+ }
103
+ }
104
+ return result;
105
+ }
106
+ /**
107
+ * Strip illegal control characters from a string value.
108
+ */
109
+ function sanitizeString(str, path, changes) {
110
+ if (!CONTROL_CHAR_REGEX.test(str))
111
+ return str;
112
+ const cleaned = str.replace(CONTROL_CHAR_REGEX, '');
113
+ changes.push(`stripped control chars at ${path || '$'}`);
114
+ return cleaned;
115
+ }
116
+ /**
117
+ * Ensure `arguments` on a function_call item is a valid JSON string.
118
+ *
119
+ * The Responses API spec requires `arguments` to be a JSON-encoded string.
120
+ * If Codex sends a malformed string (e.g. containing raw unescaped content),
121
+ * we wrap it so downstream code can safely `JSON.parse` it.
122
+ */
123
+ function fixFunctionCallArguments(item, path, changes) {
124
+ const args = item.arguments;
125
+ if (typeof args !== 'string' || args === '')
126
+ return;
127
+ // Already valid JSON — nothing to do
128
+ try {
129
+ JSON.parse(args);
130
+ return;
131
+ }
132
+ catch (_a) {
133
+ // Malformed — wrap it
134
+ }
135
+ // Wrap the raw string so JSON.parse will succeed downstream
136
+ item.arguments = JSON.stringify({ _raw: args });
137
+ changes.push(`fixed invalid arguments at ${path}`);
138
+ }
@@ -15,7 +15,7 @@
15
15
  * is driven by the ReasoningConfig passed through TransformRequestOptions.
16
16
  */
17
17
  Object.defineProperty(exports, "__esModule", { value: true });
18
- exports.processCompactResponse = exports.prepareCompactRequest = exports.buildCompactedResponse = exports.extractSummaryFromResponse = exports.buildCompactUpstreamRequest = exports.buildCompactionPrompt = exports.COMPACTION_SYSTEM_PROMPT = exports.isCodexCompactRequest = exports.isLastClaudeMessageCompact = exports.isClaudeCompactRequest = exports.extractMessageContent = exports.extractConversationText = exports.getReasoningConfig = exports.sourceTypeToFormat = exports.detectRequestFormat = void 0;
18
+ exports.processCompactResponse = exports.prepareCompactRequest = exports.buildCompactedResponse = exports.extractSummaryFromResponse = exports.buildCompactUpstreamRequest = exports.buildCompactionPrompt = exports.COMPACTION_SYSTEM_PROMPT = exports.isCodexCompactRequest = exports.isLastClaudeMessageCompact = exports.isClaudeCompactRequest = exports.extractMessageContent = exports.extractConversationText = exports.sanitizeRequestBody = exports.getServerToolSupport = exports.getReasoningConfig = exports.sourceTypeToFormat = exports.detectRequestFormat = void 0;
19
19
  exports.transformRequest = transformRequest;
20
20
  exports.transformResponse = transformResponse;
21
21
  exports.createStreamConverter = createStreamConverter;
@@ -26,6 +26,12 @@ Object.defineProperty(exports, "sourceTypeToFormat", { enumerable: true, get: fu
26
26
  var providers_js_1 = require("./thinking/providers.js");
27
27
  Object.defineProperty(exports, "getReasoningConfig", { enumerable: true, get: function () { return providers_js_1.getReasoningConfig; } });
28
28
  const providers_js_2 = require("./thinking/providers.js");
29
+ var providers_js_3 = require("./server-tool/providers.js");
30
+ Object.defineProperty(exports, "getServerToolSupport", { enumerable: true, get: function () { return providers_js_3.getServerToolSupport; } });
31
+ const mapper_js_1 = require("./server-tool/mapper.js");
32
+ // --- Body sanitizer ---
33
+ var body_sanitizer_js_1 = require("./body-sanitizer.js");
34
+ Object.defineProperty(exports, "sanitizeRequestBody", { enumerable: true, get: function () { return body_sanitizer_js_1.sanitizeRequestBody; } });
29
35
  // --- Compact API ---
30
36
  var compact_js_1 = require("./compact.js");
31
37
  Object.defineProperty(exports, "extractConversationText", { enumerable: true, get: function () { return compact_js_1.extractConversationText; } });
@@ -84,7 +90,7 @@ const request_js_13 = require("./pairs/gemini-responses/request.js");
84
90
  const response_js_12 = require("./pairs/gemini-responses/response.js");
85
91
  const streaming_js_12 = require("./pairs/gemini-responses/streaming.js");
86
92
  // --- Provider-driven post-processing ---
87
- const mapper_js_1 = require("./thinking/mapper.js");
93
+ const mapper_js_2 = require("./thinking/mapper.js");
88
94
  const effort_js_1 = require("./thinking/effort.js");
89
95
  // ============================================================
90
96
  // Public API: Request Transformation
@@ -197,63 +203,69 @@ function createStreamConverter(options) {
197
203
  * with provider-driven post-processing for completions targets.
198
204
  */
199
205
  function buildTargetBody(options) {
200
- const { fromFormat, toFormat, body, sanitizeBody, providerConfig } = options;
206
+ const { fromFormat, toFormat, sanitizeBody, providerConfig, serverToolConfig } = options;
207
+ // Pre-processing: convert server_tool_use → tool_use when upstream doesn't support it.
208
+ // Must happen before format conversion so all pair transformers handle the blocks correctly.
209
+ let processedBody = options.body;
210
+ if (fromFormat === 'claude' && !(serverToolConfig === null || serverToolConfig === void 0 ? void 0 : serverToolConfig.supportsServerToolUse)) {
211
+ processedBody = (0, mapper_js_1.convertServerToolUseToToolUse)(processedBody);
212
+ }
201
213
  // Dispatch to the correct conversion pair
202
214
  const key = `${fromFormat}->${toFormat}`;
203
215
  let result;
204
216
  switch (key) {
205
217
  // --- claude → * ---
206
218
  case 'claude->completions':
207
- result = (0, request_js_1.claudeToCompletions)(body);
219
+ result = (0, request_js_1.claudeToCompletions)(processedBody);
208
220
  break;
209
221
  case 'claude->responses':
210
- result = (0, request_js_2.claudeToResponses)(body);
222
+ result = (0, request_js_2.claudeToResponses)(processedBody);
211
223
  break;
212
224
  case 'claude->gemini':
213
- result = (0, request_js_3.claudeToGemini)(body);
225
+ result = (0, request_js_3.claudeToGemini)(processedBody);
214
226
  break;
215
227
  // --- responses → * ---
216
228
  case 'responses->completions':
217
- result = (0, request_js_8.responsesToCompletions)(body);
229
+ result = (0, request_js_8.responsesToCompletions)(processedBody);
218
230
  break;
219
231
  case 'responses->claude':
220
- result = (0, request_js_7.responsesToClaude)(body);
232
+ result = (0, request_js_7.responsesToClaude)(processedBody);
221
233
  break;
222
234
  case 'responses->gemini':
223
- result = (0, request_js_9.responsesToGeminiRequest)(body);
235
+ result = (0, request_js_9.responsesToGeminiRequest)(processedBody);
224
236
  break;
225
237
  case 'responses->responses': {
226
238
  if (sanitizeBody) {
227
239
  // Responses 格式降级兼容:委托给 responses-responses pair 处理
228
- result = (0, request_js_10.downgradeResponsesRequest)(body);
240
+ result = (0, request_js_10.downgradeResponsesRequest)(processedBody);
229
241
  }
230
242
  else {
231
- result = body;
243
+ result = processedBody;
232
244
  }
233
245
  break;
234
246
  }
235
247
  // --- completions → * ---
236
248
  case 'completions->claude':
237
- result = (0, request_js_4.completionsToClaude)(body);
249
+ result = (0, request_js_4.completionsToClaude)(processedBody);
238
250
  break;
239
251
  case 'completions->responses':
240
- result = (0, request_js_5.completionsToResponses)(body);
252
+ result = (0, request_js_5.completionsToResponses)(processedBody);
241
253
  break;
242
254
  case 'completions->gemini':
243
- result = (0, request_js_6.completionsToGemini)(body);
255
+ result = (0, request_js_6.completionsToGemini)(processedBody);
244
256
  break;
245
257
  // --- gemini → * ---
246
258
  case 'gemini->claude':
247
- result = (0, request_js_11.geminiToClaude)(body);
259
+ result = (0, request_js_11.geminiToClaude)(processedBody);
248
260
  break;
249
261
  case 'gemini->completions':
250
- result = (0, request_js_12.geminiToCompletions)(body);
262
+ result = (0, request_js_12.geminiToCompletions)(processedBody);
251
263
  break;
252
264
  case 'gemini->responses':
253
- result = (0, request_js_13.geminiToResponsesRequest)(body);
265
+ result = (0, request_js_13.geminiToResponsesRequest)(processedBody);
254
266
  break;
255
267
  default:
256
- result = body;
268
+ result = processedBody;
257
269
  }
258
270
  // --- Provider-driven post-processing for completions targets ---
259
271
  if (toFormat === 'completions' && providerConfig) {
@@ -261,21 +273,26 @@ function buildTargetBody(options) {
261
273
  if (isReasoningContentCompletion) {
262
274
  // 修复历史:确保 assistant + tool_calls 消息有 reasoning_content
263
275
  if (result.messages) {
264
- result.messages = (0, mapper_js_1.fixThinkingHistory)(result.messages, 'completions');
276
+ result.messages = (0, mapper_js_2.fixThinkingHistory)(result.messages, 'completions');
265
277
  }
266
278
  // 剥离 stream_options(reasoning_content 提供商通常不支持)
267
279
  delete result.stream_options;
268
280
  }
269
281
  // 注入 thinking 参数(如 thinking: { type: 'enabled' })和 effort 参数
270
282
  if (providerConfig.supportsThinking || providerConfig.supportsEffort) {
271
- const effort = body.thinking ? (0, effort_js_1.claudeThinkingToReasoningEffort)(body.thinking) : null;
283
+ const effort = processedBody.thinking ? (0, effort_js_1.claudeThinkingToReasoningEffort)(processedBody.thinking) : null;
272
284
  result = (0, providers_js_2.applyReasoningConfig)(result, providerConfig, effort);
273
285
  }
274
286
  }
287
+ // --- Provider-driven: convert redacted_thinking → thinking for providers that don't support redacted_thinking ---
288
+ // DeepSeek 等 provider 的 Anthropic 端点不识别 redacted_thinking,需要转换为 thinking 块
289
+ if (toFormat === 'claude' && (providerConfig === null || providerConfig === void 0 ? void 0 : providerConfig.supportsThinking) && result.messages) {
290
+ result.messages = (0, mapper_js_2.convertRedactedThinkingForProvider)(result.messages);
291
+ }
275
292
  // --- Safety net for Claude upstream: ensure thinking blocks alongside tool_use ---
276
293
  // When thinking mode is enabled, Claude requires thinking blocks in assistant messages with tool_use
277
294
  if (toFormat === 'claude' && result.thinking && result.messages) {
278
- result.messages = (0, mapper_js_1.fixThinkingHistory)(result.messages, 'claude');
295
+ result.messages = (0, mapper_js_2.fixThinkingHistory)(result.messages, 'claude');
279
296
  }
280
297
  return result;
281
298
  }
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+ /**
3
+ * Server tool use content block transformation.
4
+ *
5
+ * Converts server_tool_use blocks to regular tool_use blocks so that upstream
6
+ * providers which do not recognise the server_tool_use type can still process
7
+ * the conversation history correctly.
8
+ *
9
+ * Conversion is simple: only the `type` field changes from 'server_tool_use'
10
+ * to 'tool_use'. The `id`, `name`, and `input` fields are preserved, and
11
+ * matching `tool_result` blocks (which reference by `tool_use_id`) remain valid.
12
+ */
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.convertServerToolUseToToolUse = convertServerToolUseToToolUse;
15
+ /**
16
+ * Convert all server_tool_use content blocks in the request body to tool_use.
17
+ *
18
+ * Scans assistant messages in body.messages and replaces the block type.
19
+ * Returns a shallow-cloned body with modified messages; original body is not mutated.
20
+ */
21
+ function convertServerToolUseToToolUse(body) {
22
+ if (!(body === null || body === void 0 ? void 0 : body.messages) || !Array.isArray(body.messages)) {
23
+ return body;
24
+ }
25
+ let modified = false;
26
+ const newMessages = body.messages.map((msg) => {
27
+ // server_tool_use only appears in assistant messages
28
+ if (msg.role !== 'assistant' || !Array.isArray(msg.content)) {
29
+ return msg;
30
+ }
31
+ let msgModified = false;
32
+ const newContent = msg.content.map((block) => {
33
+ if ((block === null || block === void 0 ? void 0 : block.type) === 'server_tool_use') {
34
+ msgModified = true;
35
+ return Object.assign(Object.assign({}, block), { type: 'tool_use' });
36
+ }
37
+ return block;
38
+ });
39
+ if (msgModified) {
40
+ modified = true;
41
+ return Object.assign(Object.assign({}, msg), { content: newContent });
42
+ }
43
+ return msg;
44
+ });
45
+ if (!modified) {
46
+ return body;
47
+ }
48
+ return Object.assign(Object.assign({}, body), { messages: newMessages });
49
+ }
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ /**
3
+ * Server tool use (server_tool_use) provider support detection.
4
+ *
5
+ * server_tool_use is a Claude-specific content block type used by built-in
6
+ * server-side tools (e.g. webReader). Most third-party Claude-compatible APIs
7
+ * do not accept this type in request messages. This module detects whether the
8
+ * upstream provider supports it natively, following the same pattern as
9
+ * thinking/providers.ts.
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.SUPPORTED_CONFIG = exports.DEFAULT_CONFIG = void 0;
13
+ exports.getServerToolSupport = getServerToolSupport;
14
+ /**
15
+ * Providers known to support server_tool_use content blocks in request messages.
16
+ * Detection is based on URL / provider name substring matching.
17
+ */
18
+ const SUPPORTED_PATTERNS = [
19
+ 'api.anthropic.com',
20
+ 'anthropic',
21
+ ];
22
+ const SUPPORTED_CONFIG = { supportsServerToolUse: true };
23
+ exports.SUPPORTED_CONFIG = SUPPORTED_CONFIG;
24
+ const DEFAULT_CONFIG = { supportsServerToolUse: false };
25
+ exports.DEFAULT_CONFIG = DEFAULT_CONFIG;
26
+ /**
27
+ * Detect whether the upstream provider supports server_tool_use content blocks.
28
+ *
29
+ * @param providerName Service name (e.g. "Anthropic", "OpenRouter")
30
+ * @param baseUrl Service API URL
31
+ */
32
+ function getServerToolSupport(providerName, baseUrl) {
33
+ const haystack = `${providerName} ${baseUrl}`.toLowerCase();
34
+ for (const pattern of SUPPORTED_PATTERNS) {
35
+ if (haystack.includes(pattern)) {
36
+ return SUPPORTED_CONFIG;
37
+ }
38
+ }
39
+ return DEFAULT_CONFIG;
40
+ }
@@ -7,6 +7,7 @@ exports.thinkingToReasoningContent = thinkingToReasoningContent;
7
7
  exports.reasoningContentToThinking = reasoningContentToThinking;
8
8
  exports.reasoningToThinking = reasoningToThinking;
9
9
  exports.thinkingToReasoningSummary = thinkingToReasoningSummary;
10
+ exports.convertRedactedThinkingForProvider = convertRedactedThinkingForProvider;
10
11
  exports.fixThinkingHistory = fixThinkingHistory;
11
12
  exports.redactedThinkingPlaceholder = redactedThinkingPlaceholder;
12
13
  /** Claude thinking text → reasoning_content string */
@@ -29,6 +30,26 @@ function reasoningToThinking(summary) {
29
30
  function thinkingToReasoningSummary(thinking) {
30
31
  return [{ type: 'summary_text', text: thinking }];
31
32
  }
33
+ /**
34
+ * 将 assistant 消息中的 redacted_thinking 块转换为 thinking 块。
35
+ * 用于不支持 redacted_thinking 的上游 provider(如 DeepSeek Anthropic 端点)。
36
+ *
37
+ * DeepSeek V4 模型的 Anthropic 兼容端点在 thinking 模式下仅识别 content[].thinking,
38
+ * 不识别 redacted_thinking 类型。Claude Code 在多轮对话中会将历史 thinking 压缩为
39
+ * redacted_thinking 以节省 token,因此需要在转发前做转换。
40
+ */
41
+ function convertRedactedThinkingForProvider(messages) {
42
+ return messages.map(msg => {
43
+ if (msg.role !== 'assistant' || !Array.isArray(msg.content))
44
+ return msg;
45
+ const hasRedacted = msg.content.some((b) => b.type === 'redacted_thinking');
46
+ if (!hasRedacted)
47
+ return msg;
48
+ return Object.assign(Object.assign({}, msg), { content: msg.content.map((b) => b.type === 'redacted_thinking'
49
+ ? { type: 'thinking', thinking: '[thinking content redacted]' }
50
+ : b) });
51
+ });
52
+ }
32
53
  /** Fix history messages: ensure thinking/reasoning_content is present alongside tool use */
33
54
  function fixThinkingHistory(messages, format) {
34
55
  return messages.map(msg => {
@@ -922,7 +922,7 @@ const listInstalledSkills = () => {
922
922
  });
923
923
  return Array.from(result.values()).sort((a, b) => a.name.localeCompare(b.name, 'zh-CN'));
924
924
  };
925
- const registerRoutes = (dbManager, proxyServer) => {
925
+ const registerRoutes = (dbManager, proxyServer) => __awaiter(void 0, void 0, void 0, function* () {
926
926
  updateProxyConfig(dbManager.getConfig());
927
927
  app.get('/health', (_req, res) => res.json({ status: 'ok' }));
928
928
  // 鉴权相关路由 - 公开访问
@@ -1885,7 +1885,22 @@ ${instruction}
1885
1885
  res.json(result);
1886
1886
  })));
1887
1887
  app.put('/api/mcps/:id', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
1888
- const result = yield dbManager.updateMCP(req.params.id, req.body);
1888
+ const updateData = req.body;
1889
+ const oldMcp = dbManager.getMCP(req.params.id);
1890
+ const result = yield dbManager.updateMCP(req.params.id, updateData);
1891
+ // 如果targets发生变化,同步MCP配置到对应工具
1892
+ if (updateData.targets !== undefined) {
1893
+ const newTargets = updateData.targets;
1894
+ const oldTargets = (oldMcp === null || oldMcp === void 0 ? void 0 : oldMcp.targets) || [];
1895
+ // 需要同步的所有target(新增的 + 移除的都需要处理)
1896
+ const allAffectedTargets = new Set([...newTargets, ...oldTargets]);
1897
+ for (const target of allAffectedTargets) {
1898
+ const activeRouteId = dbManager.getActiveRouteIdForTool(target);
1899
+ if (activeRouteId) {
1900
+ yield writeMCPConfig(target);
1901
+ }
1902
+ }
1903
+ }
1889
1904
  res.json(result);
1890
1905
  })));
1891
1906
  app.delete('/api/mcps/:id', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
@@ -1947,10 +1962,83 @@ ${instruction}
1947
1962
  return true;
1948
1963
  }
1949
1964
  else if (targetType === 'codex') {
1950
- // Codex使用TOML格式,我们暂时不直接写入
1951
- // 需要后续处理Codex的MCP配置格式
1952
- // TODO: 实现 Codex MCP 配置写入
1953
- console.log('[MCP] Codex MCP配置写入暂未实现');
1965
+ // Codex使用TOML格式的 config.toml,MCP配置格式为 [mcp_servers.<name>]
1966
+ const codexDir = path_1.default.join(homeDir, '.codex');
1967
+ const codexConfigPath = path_1.default.join(codexDir, 'config.toml');
1968
+ if (!fs_1.default.existsSync(codexDir)) {
1969
+ fs_1.default.mkdirSync(codexDir, { recursive: true });
1970
+ }
1971
+ // 读取当前 config.toml
1972
+ let currentConfig = {};
1973
+ if (fs_1.default.existsSync(codexConfigPath)) {
1974
+ try {
1975
+ currentConfig = (0, config_merge_1.parseToml)(fs_1.default.readFileSync(codexConfigPath, 'utf-8'));
1976
+ }
1977
+ catch (error) {
1978
+ console.warn('[MCP] Failed to parse Codex config.toml:', error);
1979
+ }
1980
+ }
1981
+ // 清除已有的代理写入的 mcp_servers 条目(通过metadata追踪)
1982
+ const mcpMetaPath = path_1.default.join(codexDir, '.aicodeswitch_mcp_servers.json');
1983
+ let previousMcpIds = [];
1984
+ if (fs_1.default.existsSync(mcpMetaPath)) {
1985
+ try {
1986
+ previousMcpIds = JSON.parse(fs_1.default.readFileSync(mcpMetaPath, 'utf8'));
1987
+ for (const id of previousMcpIds) {
1988
+ if (currentConfig.mcp_servers && currentConfig.mcp_servers[id]) {
1989
+ delete currentConfig.mcp_servers[id];
1990
+ }
1991
+ }
1992
+ }
1993
+ catch (_a) {
1994
+ // ignore
1995
+ }
1996
+ }
1997
+ // 确保mcp_servers对象存在
1998
+ if (!currentConfig.mcp_servers) {
1999
+ currentConfig.mcp_servers = {};
2000
+ }
2001
+ // 写入所有启用的MCP
2002
+ const writtenMcpIds = [];
2003
+ for (const mcp of mcps) {
2004
+ const mcpConfig = {};
2005
+ if (mcp.type === 'stdio') {
2006
+ mcpConfig.command = mcp.command || '';
2007
+ if (mcp.args && mcp.args.length > 0) {
2008
+ mcpConfig.args = mcp.args;
2009
+ }
2010
+ // stdio 类型的环境变量写在 [mcp_servers.name.env] 子表中
2011
+ if (mcp.env && Object.keys(mcp.env).length > 0) {
2012
+ mcpConfig.env = Object.assign({}, mcp.env);
2013
+ }
2014
+ }
2015
+ else if (mcp.type === 'http') {
2016
+ // Codex 使用 Streamable HTTP 传输,url 字段
2017
+ mcpConfig.url = mcp.url || '';
2018
+ // HTTP 类型可选的 headers
2019
+ if (mcp.headers && Object.keys(mcp.headers).length > 0) {
2020
+ mcpConfig.headers = Object.assign({}, mcp.headers);
2021
+ }
2022
+ }
2023
+ else if (mcp.type === 'sse') {
2024
+ // SSE 传输也使用 url 字段
2025
+ mcpConfig.url = mcp.url || '';
2026
+ if (mcp.headers && Object.keys(mcp.headers).length > 0) {
2027
+ mcpConfig.headers = Object.assign({}, mcp.headers);
2028
+ }
2029
+ }
2030
+ currentConfig.mcp_servers[mcp.id] = mcpConfig;
2031
+ writtenMcpIds.push(mcp.id);
2032
+ }
2033
+ // 如果mcp_servers为空对象,删除该键
2034
+ if (Object.keys(currentConfig.mcp_servers).length === 0) {
2035
+ delete currentConfig.mcp_servers;
2036
+ }
2037
+ // 写回 config.toml
2038
+ (0, config_merge_1.atomicWriteFile)(codexConfigPath, (0, config_merge_1.stringifyToml)(currentConfig));
2039
+ // 保存已写入的MCP ID列表,用于后续清理
2040
+ fs_1.default.writeFileSync(mcpMetaPath, JSON.stringify(writtenMcpIds, null, 2));
2041
+ console.log(`[MCP] Codex MCP config written: ${writtenMcpIds.length} server(s)`);
1954
2042
  return true;
1955
2043
  }
1956
2044
  return false;
@@ -1976,6 +2064,45 @@ ${instruction}
1976
2064
  }
1977
2065
  return true;
1978
2066
  }
2067
+ else if (targetType === 'codex') {
2068
+ // 从 Codex config.toml 中移除指定的 MCP 条目
2069
+ const homeDir = os_1.default.homedir();
2070
+ const codexDir = path_1.default.join(homeDir, '.codex');
2071
+ const codexConfigPath = path_1.default.join(codexDir, 'config.toml');
2072
+ if (!fs_1.default.existsSync(codexConfigPath)) {
2073
+ return true;
2074
+ }
2075
+ let currentConfig = {};
2076
+ try {
2077
+ currentConfig = (0, config_merge_1.parseToml)(fs_1.default.readFileSync(codexConfigPath, 'utf-8'));
2078
+ }
2079
+ catch (error) {
2080
+ console.warn('[MCP] Failed to parse Codex config.toml for removal:', error);
2081
+ return false;
2082
+ }
2083
+ if (currentConfig.mcp_servers && currentConfig.mcp_servers[mcpId]) {
2084
+ delete currentConfig.mcp_servers[mcpId];
2085
+ // 如果mcp_servers为空对象,删除该键
2086
+ if (Object.keys(currentConfig.mcp_servers).length === 0) {
2087
+ delete currentConfig.mcp_servers;
2088
+ }
2089
+ (0, config_merge_1.atomicWriteFile)(codexConfigPath, (0, config_merge_1.stringifyToml)(currentConfig));
2090
+ // 更新metadata
2091
+ const mcpMetaPath = path_1.default.join(codexDir, '.aicodeswitch_mcp_servers.json');
2092
+ if (fs_1.default.existsSync(mcpMetaPath)) {
2093
+ try {
2094
+ const previousIds = JSON.parse(fs_1.default.readFileSync(mcpMetaPath, 'utf8'));
2095
+ const updatedIds = previousIds.filter(id => id !== mcpId);
2096
+ fs_1.default.writeFileSync(mcpMetaPath, JSON.stringify(updatedIds, null, 2));
2097
+ }
2098
+ catch (_a) {
2099
+ // ignore
2100
+ }
2101
+ }
2102
+ console.log(`[MCP] Removed MCP ${mcpId} from Codex config`);
2103
+ }
2104
+ return true;
2105
+ }
1979
2106
  return false;
1980
2107
  }
1981
2108
  catch (error) {
@@ -1983,7 +2110,29 @@ ${instruction}
1983
2110
  return false;
1984
2111
  }
1985
2112
  });
1986
- };
2113
+ // 服务启动时同步MCP配置到已激活的工具
2114
+ const allMcps = dbManager.getMCPs();
2115
+ const targetsToSync = new Set();
2116
+ for (const mcp of allMcps) {
2117
+ if (mcp.targets) {
2118
+ for (const target of mcp.targets) {
2119
+ targetsToSync.add(target);
2120
+ }
2121
+ }
2122
+ }
2123
+ for (const target of targetsToSync) {
2124
+ const activeRouteId = dbManager.getActiveRouteIdForTool(target);
2125
+ if (activeRouteId) {
2126
+ try {
2127
+ yield writeMCPConfig(target);
2128
+ console.log(`[Startup MCP Sync] MCP config synced for ${target}`);
2129
+ }
2130
+ catch (error) {
2131
+ console.error(`[Startup MCP Sync] Failed to sync MCP config for ${target}:`, error);
2132
+ }
2133
+ }
2134
+ }
2135
+ });
1987
2136
  const start = () => __awaiter(void 0, void 0, void 0, function* () {
1988
2137
  fs_1.default.mkdirSync(dataDir, { recursive: true });
1989
2138
  // 自动检测数据库类型并执行迁移(如果需要)
@@ -2003,7 +2152,7 @@ const start = () => __awaiter(void 0, void 0, void 0, function* () {
2003
2152
  // Initialize proxy server and register proxy routes last
2004
2153
  proxyServer.initialize();
2005
2154
  // Register admin routes first
2006
- registerRoutes(dbManager, proxyServer);
2155
+ yield registerRoutes(dbManager, proxyServer);
2007
2156
  yield proxyServer.registerProxyRoutes();
2008
2157
  app.use(express_1.default.static(path_1.default.resolve(__dirname, '../ui')));
2009
2158
  // 404 处理程序 - 确保返回 JSON 而不是 HTML(放在所有路由和静态文件之后)
@@ -2761,10 +2761,10 @@ class ProxyServer {
2761
2761
  * @param targetModel 目标模型名称(可选)
2762
2762
  * @returns 转换后往服务商API接口的数据
2763
2763
  */
2764
- transformRequestToUpstream(tool, source, payloadData, targetModel, providerConfig) {
2764
+ transformRequestToUpstream(tool, source, payloadData, targetModel, providerConfig, serverToolConfig) {
2765
2765
  const clientFormat = tool === 'codex' ? 'responses' : 'claude';
2766
2766
  const upstreamFormat = (0, index_1.sourceTypeToFormat)(source);
2767
- const result = (0, index_1.transformRequest)({ fromFormat: clientFormat, toFormat: upstreamFormat, body: payloadData, providerConfig });
2767
+ const result = (0, index_1.transformRequest)({ fromFormat: clientFormat, toFormat: upstreamFormat, body: payloadData, providerConfig, serverToolConfig });
2768
2768
  const body = result.body;
2769
2769
  // 模型覆盖:OpenAI 模型族保持原样,其余覆盖为 targetModel
2770
2770
  if (targetModel) {
@@ -2898,6 +2898,12 @@ class ProxyServer {
2898
2898
  const useOriginalConfig = (options === null || options === void 0 ? void 0 : options.useOriginalConfig) === true;
2899
2899
  let relayedForLog = !useOriginalConfig;
2900
2900
  let originalToolRequestBody = this.cloneRequestBody(req.body || {});
2901
+ // 请求体安全性清理:修复控制字符、无效 JSON arguments、undefined 值等问题
2902
+ const sanitizeResult = (0, index_1.sanitizeRequestBody)(originalToolRequestBody);
2903
+ if (sanitizeResult.changes.length > 0) {
2904
+ console.log(`[Body-Sanitize] ${sanitizeResult.changes.length} fix(es): ${sanitizeResult.changes.join('; ')}`);
2905
+ }
2906
+ originalToolRequestBody = sanitizeResult.body;
2901
2907
  let requestBody = this.cloneRequestBody(originalToolRequestBody) || {};
2902
2908
  let usageForLog;
2903
2909
  let logged = false;
@@ -3286,7 +3292,8 @@ class ProxyServer {
3286
3292
  const effectiveApiUrl = this.resolveEffectiveApiUrl(service);
3287
3293
  const effectiveModel = rule.targetModel || (requestBody === null || requestBody === void 0 ? void 0 : requestBody.model);
3288
3294
  const providerConfig = (0, index_1.getReasoningConfig)(service.name || '', effectiveApiUrl || '', effectiveModel || '');
3289
- const transformedRequestBody = this.transformRequestToUpstream(targetType, sourceType, payloadForTransform, rule.targetModel, providerConfig);
3295
+ const serverToolConfig = (0, index_1.getServerToolSupport)(service.name || '', effectiveApiUrl || '');
3296
+ const transformedRequestBody = this.transformRequestToUpstream(targetType, sourceType, payloadForTransform, rule.targetModel, providerConfig, serverToolConfig);
3290
3297
  requestBody = (_b = transformedRequestBody !== null && transformedRequestBody !== void 0 ? transformedRequestBody : this.cloneRequestBody(originalToolRequestBody)) !== null && _b !== void 0 ? _b : {};
3291
3298
  // 对最终即将发送到上游的 Claude compact 请求再做一次兜底清理,
3292
3299
  // 避免中间转换/覆盖步骤重新引入未配对的 tool_use。
@@ -3921,6 +3928,12 @@ class ProxyServer {
3921
3928
  console.log(`\x1b[32m[ApiPathProxy]\x1b[0m path=${apiPath}, clientFormat=${clientFormat}, session=-, rule=${rule.id}(${rule.contentType}), vendor=${(vendor === null || vendor === void 0 ? void 0 : vendor.name) || '-'}, service=${service.name}`);
3922
3929
  const failoverEnabled = (options === null || options === void 0 ? void 0 : options.failoverEnabled) === true;
3923
3930
  let requestBody = this.cloneRequestBody(req.body || {});
3931
+ // 请求体安全性清理:修复控制字符、无效 JSON arguments、undefined 值等问题
3932
+ const sanitizeResult = (0, index_1.sanitizeRequestBody)(requestBody);
3933
+ if (sanitizeResult.changes.length > 0) {
3934
+ console.log(`[Body-Sanitize] ${sanitizeResult.changes.length} fix(es): ${sanitizeResult.changes.join('; ')}`);
3935
+ }
3936
+ requestBody = sanitizeResult.body;
3924
3937
  let usageForLog;
3925
3938
  let responseBodyForLog;
3926
3939
  let downstreamResponseBodyForLog;
@@ -3972,7 +3985,8 @@ class ProxyServer {
3972
3985
  const effectiveApiUrl = this.resolveEffectiveApiUrl(service);
3973
3986
  const effectiveModel = rule.targetModel || (requestBody === null || requestBody === void 0 ? void 0 : requestBody.model);
3974
3987
  const providerConfig = (0, index_1.getReasoningConfig)(service.name || '', effectiveApiUrl || '', effectiveModel || '');
3975
- const transformedRequestBody = this.transformRequestByFormat(clientFormat, sourceType, payloadForTransform, rule.targetModel, providerConfig);
3988
+ const serverToolConfig = (0, index_1.getServerToolSupport)(service.name || '', effectiveApiUrl || '');
3989
+ const transformedRequestBody = this.transformRequestByFormat(clientFormat, sourceType, payloadForTransform, rule.targetModel, providerConfig, serverToolConfig);
3976
3990
  requestBody = (_a = transformedRequestBody !== null && transformedRequestBody !== void 0 ? transformedRequestBody : this.cloneRequestBody(requestBody)) !== null && _a !== void 0 ? _a : {};
3977
3991
  // Compact final sanitize
3978
3992
  if (rule.contentType === 'compact' && clientFormat === 'claude' && Array.isArray(requestBody === null || requestBody === void 0 ? void 0 : requestBody.messages)) {
@@ -4184,9 +4198,9 @@ class ProxyServer {
4184
4198
  /**
4185
4199
  * 使用显式 clientFormat 进行请求转换(取代 tool → format 的硬编码映射)
4186
4200
  */
4187
- transformRequestByFormat(clientFormat, source, payloadData, targetModel, providerConfig) {
4201
+ transformRequestByFormat(clientFormat, source, payloadData, targetModel, providerConfig, serverToolConfig) {
4188
4202
  const upstreamFormat = (0, index_1.sourceTypeToFormat)(source);
4189
- const result = (0, index_1.transformRequest)({ fromFormat: clientFormat, toFormat: upstreamFormat, body: payloadData, providerConfig });
4203
+ const result = (0, index_1.transformRequest)({ fromFormat: clientFormat, toFormat: upstreamFormat, body: payloadData, providerConfig, serverToolConfig });
4190
4204
  const body = result.body;
4191
4205
  if (targetModel) {
4192
4206
  const isOpenAIModel = /^gpt-|o[123]/i.test(targetModel);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aicodeswitch",
3
- "version": "5.1.1",
3
+ "version": "5.1.2",
4
4
  "description": "A tool to help you manage AI programming tools to access large language models locally. It allows your Claude Code, Codex and other tools to no longer be limited to official models.",
5
5
  "author": "tangshuang",
6
6
  "license": "GPL-3.0",