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.
- package/dist/server/coding-plan-headers.js +121 -0
- package/dist/server/config-managed-fields.js +1 -0
- package/dist/server/conversions/body-sanitizer.js +138 -0
- package/dist/server/conversions/index.js +46 -21
- package/dist/server/conversions/server-tool/mapper.js +49 -0
- package/dist/server/conversions/server-tool/providers.js +40 -0
- package/dist/server/conversions/thinking/mapper.js +21 -0
- package/dist/server/conversions/utils/tool-result.js +35 -0
- package/dist/server/fs-database.js +58 -0
- package/dist/server/main.js +308 -8
- package/dist/server/proxy-server.js +91 -14
- package/dist/server/rules-status-service.js +16 -0
- package/dist/server/session-launcher.js +282 -0
- package/dist/server/session-migration.js +419 -0
- package/dist/server/transformers/chunk-collector.js +28 -1
- package/dist/ui/assets/claude-XtpLmGtF.webp +0 -0
- package/dist/ui/assets/index-CMoQtBmK.css +1 -0
- package/dist/ui/assets/index-CXdNTFiX.js +532 -0
- package/dist/ui/assets/openai-CPEiZpaN.webp +0 -0
- package/dist/ui/index.html +2 -2
- package/package.json +1 -1
- package/dist/ui/assets/index-BHR12ImE.css +0 -1
- package/dist/ui/assets/index-CumAhpXg.js +0 -517
|
@@ -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
|
|
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,
|
|
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)(
|
|
220
|
+
result = (0, request_js_1.claudeToCompletions)(processedBody);
|
|
208
221
|
break;
|
|
209
222
|
case 'claude->responses':
|
|
210
|
-
result = (0, request_js_2.claudeToResponses)(
|
|
223
|
+
result = (0, request_js_2.claudeToResponses)(processedBody);
|
|
211
224
|
break;
|
|
212
225
|
case 'claude->gemini':
|
|
213
|
-
result = (0, request_js_3.claudeToGemini)(
|
|
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)(
|
|
230
|
+
result = (0, request_js_8.responsesToCompletions)(processedBody);
|
|
218
231
|
break;
|
|
219
232
|
case 'responses->claude':
|
|
220
|
-
result = (0, request_js_7.responsesToClaude)(
|
|
233
|
+
result = (0, request_js_7.responsesToClaude)(processedBody);
|
|
221
234
|
break;
|
|
222
235
|
case 'responses->gemini':
|
|
223
|
-
result = (0, request_js_9.responsesToGeminiRequest)(
|
|
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)(
|
|
241
|
+
result = (0, request_js_10.downgradeResponsesRequest)(processedBody);
|
|
229
242
|
}
|
|
230
243
|
else {
|
|
231
|
-
result =
|
|
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)(
|
|
250
|
+
result = (0, request_js_4.completionsToClaude)(processedBody);
|
|
238
251
|
break;
|
|
239
252
|
case 'completions->responses':
|
|
240
|
-
result = (0, request_js_5.completionsToResponses)(
|
|
253
|
+
result = (0, request_js_5.completionsToResponses)(processedBody);
|
|
241
254
|
break;
|
|
242
255
|
case 'completions->gemini':
|
|
243
|
-
result = (0, request_js_6.completionsToGemini)(
|
|
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)(
|
|
260
|
+
result = (0, request_js_11.geminiToClaude)(processedBody);
|
|
248
261
|
break;
|
|
249
262
|
case 'gemini->completions':
|
|
250
|
-
result = (0, request_js_12.geminiToCompletions)(
|
|
263
|
+
result = (0, request_js_12.geminiToCompletions)(processedBody);
|
|
251
264
|
break;
|
|
252
265
|
case 'gemini->responses':
|
|
253
|
-
result = (0, request_js_13.geminiToResponsesRequest)(
|
|
266
|
+
result = (0, request_js_13.geminiToResponsesRequest)(processedBody);
|
|
254
267
|
break;
|
|
255
268
|
default:
|
|
256
|
-
result =
|
|
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,
|
|
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 =
|
|
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,
|
|
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* () {
|