aicodeswitch 5.1.1 → 5.1.3

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.
@@ -0,0 +1,121 @@
1
+ "use strict";
2
+ /**
3
+ * 编程套餐 Headers 覆盖模块
4
+ *
5
+ * 当 APIService 启用 enableCodingPlan 时,将发送到上游的请求 Headers
6
+ * 覆盖为对应编程工具(Claude Code / Codex)的标准 Headers,
7
+ * 使供应商验证通过。
8
+ */
9
+ var __importDefault = (this && this.__importDefault) || function (mod) {
10
+ return (mod && mod.__esModule) ? mod : { "default": mod };
11
+ };
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.applyCodingPlanHeaders = applyCodingPlanHeaders;
14
+ const crypto_1 = __importDefault(require("crypto"));
15
+ /**
16
+ * 代理已设置的需要保留的 Headers
17
+ * 这些 Headers 由 buildUpstreamHeaders 设置,不能被覆盖删除
18
+ */
19
+ const KEEP_HEADERS = new Set([
20
+ 'authorization', // 认证头
21
+ 'x-api-key', // Claude 认证头
22
+ 'x-goog-api-key', // Gemini 认证头
23
+ 'content-type', // 内容类型
24
+ 'accept', // 接受类型
25
+ 'accept-encoding', // 编码
26
+ 'connection', // 连接
27
+ 'content-length', // 内容长度
28
+ 'anthropic-version', // Claude API 版本
29
+ ]);
30
+ /**
31
+ * 构建 Claude Code 标准请求 Headers
32
+ */
33
+ function buildClaudeCodeHeaders(sessionId) {
34
+ return {
35
+ 'user-agent': 'claude-cli/2.1.168 (external, claude-vscode, agent-sdk/0.3.168)',
36
+ 'x-claude-code-session-id': sessionId,
37
+ 'x-stainless-arch': 'arm64',
38
+ 'x-stainless-lang': 'js',
39
+ 'x-stainless-os': 'MacOS',
40
+ 'x-stainless-package-version': '0.94.0',
41
+ 'x-stainless-retry-count': '0',
42
+ 'x-stainless-runtime': 'node',
43
+ 'x-stainless-runtime-version': 'v24.3.0',
44
+ 'x-stainless-timeout': '3000',
45
+ 'anthropic-beta': 'claude-code-20250219,context-1m-2025-08-07,interleaved-thinking-2025-05-14,context-management-2025-06-27,prompt-caching-scope-2026-01-05,mid-conversation-system-2026-04-07,effort-2025-11-24',
46
+ 'anthropic-dangerous-direct-browser-access': 'true',
47
+ 'x-app': 'cli',
48
+ };
49
+ }
50
+ /**
51
+ * 构建 Codex 标准请求 Headers
52
+ */
53
+ function buildCodexHeaders(sessionId) {
54
+ return {
55
+ 'x-codex-beta-features': 'terminal_resize_reflow,remote_compaction_v2',
56
+ 'x-codex-turn-metadata': JSON.stringify({
57
+ session_id: sessionId,
58
+ thread_id: sessionId,
59
+ thread_source: 'user',
60
+ turn_id: crypto_1.default.randomUUID(),
61
+ sandbox: 'none',
62
+ workspace_kind: 'project',
63
+ request_kind: 'turn',
64
+ }),
65
+ 'x-codex-window-id': `${sessionId}:0`,
66
+ 'x-client-request-id': sessionId,
67
+ 'session-id': sessionId,
68
+ 'thread-id': sessionId,
69
+ 'originator': 'codex_vscode',
70
+ 'user-agent': 'codex_vscode/0.137.0-alpha.4 (Mac OS 26.5.0; arm64) unknown (VS Code; 26.602.40724)',
71
+ };
72
+ }
73
+ /**
74
+ * 判断 sourceType 是否为 Claude 源
75
+ */
76
+ function isClaudeSourceType(sourceType) {
77
+ return sourceType === 'claude' || sourceType === 'claude-chat';
78
+ }
79
+ /**
80
+ * 判断 sourceType 是否为 OpenAI 源
81
+ */
82
+ function isOpenAISourceType(sourceType) {
83
+ return sourceType === 'openai' || sourceType === 'openai-chat';
84
+ }
85
+ /**
86
+ * 应用编程工具 Headers 覆盖
87
+ *
88
+ * 当 service.enableCodingPlan 为 true 时调用。
89
+ * 清除原始请求中无关的 Headers,注入对应编程工具的标准 Headers。
90
+ *
91
+ * - Claude 源(claude/claude-chat)→ 注入 Claude Code Headers
92
+ * - OpenAI 源(openai/openai-chat)→ 注入 Codex Headers
93
+ * - Gemini 源不处理,保持原样
94
+ *
95
+ * @param headers 当前已构建的上游 Headers(会被原地修改)
96
+ * @param sourceType 上游服务的源类型
97
+ */
98
+ function applyCodingPlanHeaders(headers, sourceType) {
99
+ const isClaude = isClaudeSourceType(sourceType);
100
+ const isOpenAI = isOpenAISourceType(sourceType);
101
+ // Gemini 源不需要 Headers 覆盖
102
+ if (!isClaude && !isOpenAI) {
103
+ return;
104
+ }
105
+ const sessionId = crypto_1.default.randomUUID();
106
+ // 1. 删除不在保留列表中的 Headers
107
+ for (const key of Object.keys(headers)) {
108
+ if (!KEEP_HEADERS.has(key.toLowerCase())) {
109
+ delete headers[key];
110
+ }
111
+ }
112
+ // 2. 注入编程工具标准 Headers
113
+ // 2. 注入编程工具标准 Headers
114
+ const toolHeaders = isClaude
115
+ ? buildClaudeCodeHeaders(sessionId)
116
+ : buildCodexHeaders(sessionId);
117
+ for (const [key, value] of Object.entries(toolHeaders)) {
118
+ headers[key] = value;
119
+ }
120
+ console.log(`\x1b[36m[CodingPlan-Headers]\x1b[0m Applied ${isClaude ? 'Claude Code' : 'Codex'} header override for upstream sourceType=${sourceType}`);
121
+ }
@@ -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,8 @@ 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");
94
+ const tool_result_js_1 = require("./utils/tool-result.js");
88
95
  const effort_js_1 = require("./thinking/effort.js");
89
96
  // ============================================================
90
97
  // Public API: Request Transformation
@@ -197,63 +204,69 @@ function createStreamConverter(options) {
197
204
  * with provider-driven post-processing for completions targets.
198
205
  */
199
206
  function buildTargetBody(options) {
200
- const { fromFormat, toFormat, body, sanitizeBody, providerConfig } = options;
207
+ const { fromFormat, toFormat, sanitizeBody, providerConfig, serverToolConfig } = options;
208
+ // Pre-processing: convert server_tool_use → tool_use when upstream doesn't support it.
209
+ // Must happen before format conversion so all pair transformers handle the blocks correctly.
210
+ let processedBody = options.body;
211
+ if (fromFormat === 'claude' && !(serverToolConfig === null || serverToolConfig === void 0 ? void 0 : serverToolConfig.supportsServerToolUse)) {
212
+ processedBody = (0, mapper_js_1.convertServerToolUseToToolUse)(processedBody);
213
+ }
201
214
  // Dispatch to the correct conversion pair
202
215
  const key = `${fromFormat}->${toFormat}`;
203
216
  let result;
204
217
  switch (key) {
205
218
  // --- claude → * ---
206
219
  case 'claude->completions':
207
- result = (0, request_js_1.claudeToCompletions)(body);
220
+ result = (0, request_js_1.claudeToCompletions)(processedBody);
208
221
  break;
209
222
  case 'claude->responses':
210
- result = (0, request_js_2.claudeToResponses)(body);
223
+ result = (0, request_js_2.claudeToResponses)(processedBody);
211
224
  break;
212
225
  case 'claude->gemini':
213
- result = (0, request_js_3.claudeToGemini)(body);
226
+ result = (0, request_js_3.claudeToGemini)(processedBody);
214
227
  break;
215
228
  // --- responses → * ---
216
229
  case 'responses->completions':
217
- result = (0, request_js_8.responsesToCompletions)(body);
230
+ result = (0, request_js_8.responsesToCompletions)(processedBody);
218
231
  break;
219
232
  case 'responses->claude':
220
- result = (0, request_js_7.responsesToClaude)(body);
233
+ result = (0, request_js_7.responsesToClaude)(processedBody);
221
234
  break;
222
235
  case 'responses->gemini':
223
- result = (0, request_js_9.responsesToGeminiRequest)(body);
236
+ result = (0, request_js_9.responsesToGeminiRequest)(processedBody);
224
237
  break;
225
238
  case 'responses->responses': {
226
239
  if (sanitizeBody) {
227
240
  // Responses 格式降级兼容:委托给 responses-responses pair 处理
228
- result = (0, request_js_10.downgradeResponsesRequest)(body);
241
+ result = (0, request_js_10.downgradeResponsesRequest)(processedBody);
229
242
  }
230
243
  else {
231
- result = body;
244
+ result = processedBody;
232
245
  }
233
246
  break;
234
247
  }
235
248
  // --- completions → * ---
236
249
  case 'completions->claude':
237
- result = (0, request_js_4.completionsToClaude)(body);
250
+ result = (0, request_js_4.completionsToClaude)(processedBody);
238
251
  break;
239
252
  case 'completions->responses':
240
- result = (0, request_js_5.completionsToResponses)(body);
253
+ result = (0, request_js_5.completionsToResponses)(processedBody);
241
254
  break;
242
255
  case 'completions->gemini':
243
- result = (0, request_js_6.completionsToGemini)(body);
256
+ result = (0, request_js_6.completionsToGemini)(processedBody);
244
257
  break;
245
258
  // --- gemini → * ---
246
259
  case 'gemini->claude':
247
- result = (0, request_js_11.geminiToClaude)(body);
260
+ result = (0, request_js_11.geminiToClaude)(processedBody);
248
261
  break;
249
262
  case 'gemini->completions':
250
- result = (0, request_js_12.geminiToCompletions)(body);
263
+ result = (0, request_js_12.geminiToCompletions)(processedBody);
251
264
  break;
252
265
  case 'gemini->responses':
253
- result = (0, request_js_13.geminiToResponsesRequest)(body);
266
+ result = (0, request_js_13.geminiToResponsesRequest)(processedBody);
254
267
  break;
255
268
  default:
256
- result = body;
269
+ result = processedBody;
257
270
  }
258
271
  // --- Provider-driven post-processing for completions targets ---
259
272
  if (toFormat === 'completions' && providerConfig) {
@@ -261,21 +274,33 @@ function buildTargetBody(options) {
261
274
  if (isReasoningContentCompletion) {
262
275
  // 修复历史:确保 assistant + tool_calls 消息有 reasoning_content
263
276
  if (result.messages) {
264
- result.messages = (0, mapper_js_1.fixThinkingHistory)(result.messages, 'completions');
277
+ result.messages = (0, mapper_js_2.fixThinkingHistory)(result.messages, 'completions');
265
278
  }
266
279
  // 剥离 stream_options(reasoning_content 提供商通常不支持)
267
280
  delete result.stream_options;
268
281
  }
269
282
  // 注入 thinking 参数(如 thinking: { type: 'enabled' })和 effort 参数
270
283
  if (providerConfig.supportsThinking || providerConfig.supportsEffort) {
271
- const effort = body.thinking ? (0, effort_js_1.claudeThinkingToReasoningEffort)(body.thinking) : null;
284
+ const effort = processedBody.thinking ? (0, effort_js_1.claudeThinkingToReasoningEffort)(processedBody.thinking) : null;
272
285
  result = (0, providers_js_2.applyReasoningConfig)(result, providerConfig, effort);
273
286
  }
274
287
  }
288
+ // --- Provider-driven: convert redacted_thinking → thinking for providers that don't support redacted_thinking ---
289
+ // DeepSeek 等 provider 的 Anthropic 端点不识别 redacted_thinking,需要转换为 thinking 块
290
+ if (toFormat === 'claude' && (providerConfig === null || providerConfig === void 0 ? void 0 : providerConfig.supportsThinking) && result.messages) {
291
+ result.messages = (0, mapper_js_2.convertRedactedThinkingForProvider)(result.messages);
292
+ }
275
293
  // --- Safety net for Claude upstream: ensure thinking blocks alongside tool_use ---
276
294
  // When thinking mode is enabled, Claude requires thinking blocks in assistant messages with tool_use
277
295
  if (toFormat === 'claude' && result.thinking && result.messages) {
278
- result.messages = (0, mapper_js_1.fixThinkingHistory)(result.messages, 'claude');
296
+ result.messages = (0, mapper_js_2.fixThinkingHistory)(result.messages, 'claude');
297
+ }
298
+ // --- Ensure tool_result blocks have id for Claude-compatible providers ---
299
+ // Some providers (e.g. GLM) require an id field on tool_result content blocks,
300
+ // but standard Claude API tool_result blocks only have tool_use_id without id.
301
+ if (toFormat === 'claude' && result.messages) {
302
+ const { messages: patchedMessages } = (0, tool_result_js_1.ensureToolResultIds)(result.messages);
303
+ result.messages = patchedMessages;
279
304
  }
280
305
  return result;
281
306
  }
@@ -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 => {
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ /**
3
+ * Tool result content block utilities.
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.ensureToolResultIds = ensureToolResultIds;
7
+ /**
8
+ * 为所有缺少 id 的 tool_result 块补上 id。
9
+ *
10
+ * 部分 Claude 兼容端点(如 GLM)要求 tool_result 内容块必须包含 id 字段,
11
+ * 但标准 Claude API 的 tool_result 块仅有 tool_use_id 而不带 id。
12
+ *
13
+ * id 取值策略:优先使用 tool_use_id(与对应的 tool_use.id 保持一致),
14
+ * 若 tool_use_id 也不存在则生成唯一 id。
15
+ */
16
+ function ensureToolResultIds(messages) {
17
+ let totalPatched = 0;
18
+ const result = messages.map(msg => {
19
+ if (msg.role !== 'user' || !Array.isArray(msg.content))
20
+ return msg;
21
+ let patched = false;
22
+ const newContent = msg.content.map((b) => {
23
+ if (b.type === 'tool_result' && !b.id) {
24
+ patched = true;
25
+ totalPatched++;
26
+ // 使用 tool_use_id 作为 id,保持与对应 tool_use 块的 id 一致
27
+ const id = b.tool_use_id || `toolu_${crypto.randomUUID().replace(/-/g, '').slice(0, 24)}`;
28
+ return Object.assign(Object.assign({}, b), { id });
29
+ }
30
+ return b;
31
+ });
32
+ return patched ? Object.assign(Object.assign({}, msg), { content: newContent }) : msg;
33
+ });
34
+ return { messages: result, patchedCount: totalPatched };
35
+ }
@@ -1597,6 +1597,18 @@ class FileSystemDatabaseManager {
1597
1597
  yield this.saveRules();
1598
1598
  this.routes.splice(index, 1);
1599
1599
  yield this.saveRoutes();
1600
+ // 级联清理:清除绑定到该路由的会话的绑定关系
1601
+ let sessionChanged = false;
1602
+ for (const session of this.sessions) {
1603
+ if (session.routeId === id) {
1604
+ session.routeId = undefined;
1605
+ session.routeName = undefined;
1606
+ sessionChanged = true;
1607
+ }
1608
+ }
1609
+ if (sessionChanged) {
1610
+ yield this.saveSessions();
1611
+ }
1600
1612
  return true;
1601
1613
  });
1602
1614
  }
@@ -3052,6 +3064,11 @@ class FileSystemDatabaseManager {
3052
3064
  existing.highIqRuleId = session.highIqRuleId;
3053
3065
  if (Object.prototype.hasOwnProperty.call(session, 'highIqEnabledAt'))
3054
3066
  existing.highIqEnabledAt = session.highIqEnabledAt;
3067
+ // 保留已有的路由绑定(不传入时不覆盖)
3068
+ if (session.routeId !== undefined)
3069
+ existing.routeId = session.routeId;
3070
+ if (session.routeName !== undefined)
3071
+ existing.routeName = session.routeName;
3055
3072
  }
3056
3073
  else {
3057
3074
  // 创建新 session
@@ -3071,11 +3088,52 @@ class FileSystemDatabaseManager {
3071
3088
  highIqMode: session.highIqMode,
3072
3089
  highIqRuleId: session.highIqRuleId,
3073
3090
  highIqEnabledAt: session.highIqEnabledAt,
3091
+ routeId: session.routeId,
3092
+ routeName: session.routeName,
3074
3093
  });
3075
3094
  }
3076
3095
  // 异步保存(不阻塞)
3077
3096
  this.saveSessions().catch(console.error);
3078
3097
  }
3098
+ /**
3099
+ * 绑定会话到路由
3100
+ */
3101
+ bindSessionRoute(sessionId, routeId) {
3102
+ return __awaiter(this, void 0, void 0, function* () {
3103
+ const session = this.sessions.find(s => s.id === sessionId);
3104
+ if (!session)
3105
+ return null;
3106
+ const route = this.routes.find(r => r.id === routeId);
3107
+ if (!route)
3108
+ return null;
3109
+ session.routeId = routeId;
3110
+ session.routeName = route.name;
3111
+ yield this.saveSessions();
3112
+ return session;
3113
+ });
3114
+ }
3115
+ /**
3116
+ * 解绑会话路由
3117
+ */
3118
+ unbindSessionRoute(sessionId) {
3119
+ return __awaiter(this, void 0, void 0, function* () {
3120
+ const session = this.sessions.find(s => s.id === sessionId);
3121
+ if (!session)
3122
+ return false;
3123
+ session.routeId = undefined;
3124
+ session.routeName = undefined;
3125
+ yield this.saveSessions();
3126
+ return true;
3127
+ });
3128
+ }
3129
+ /**
3130
+ * 获取绑定到指定路由的所有会话
3131
+ */
3132
+ getBoundSessions(routeId) {
3133
+ return this.sessions
3134
+ .filter(s => s.routeId === routeId)
3135
+ .sort((a, b) => b.lastRequestAt - a.lastRequestAt);
3136
+ }
3079
3137
  // 新增方法:获取规则黑名单状态
3080
3138
  getRuleBlacklistStatus(serviceId, routeId, contentType) {
3081
3139
  return __awaiter(this, void 0, void 0, function* () {