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.
- package/dist/server/config-managed-fields.js +1 -0
- package/dist/server/conversions/body-sanitizer.js +138 -0
- package/dist/server/conversions/index.js +38 -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/main.js +157 -8
- package/dist/server/proxy-server.js +20 -6
- package/package.json +1 -1
|
@@ -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
|
|
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,
|
|
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)(
|
|
219
|
+
result = (0, request_js_1.claudeToCompletions)(processedBody);
|
|
208
220
|
break;
|
|
209
221
|
case 'claude->responses':
|
|
210
|
-
result = (0, request_js_2.claudeToResponses)(
|
|
222
|
+
result = (0, request_js_2.claudeToResponses)(processedBody);
|
|
211
223
|
break;
|
|
212
224
|
case 'claude->gemini':
|
|
213
|
-
result = (0, request_js_3.claudeToGemini)(
|
|
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)(
|
|
229
|
+
result = (0, request_js_8.responsesToCompletions)(processedBody);
|
|
218
230
|
break;
|
|
219
231
|
case 'responses->claude':
|
|
220
|
-
result = (0, request_js_7.responsesToClaude)(
|
|
232
|
+
result = (0, request_js_7.responsesToClaude)(processedBody);
|
|
221
233
|
break;
|
|
222
234
|
case 'responses->gemini':
|
|
223
|
-
result = (0, request_js_9.responsesToGeminiRequest)(
|
|
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)(
|
|
240
|
+
result = (0, request_js_10.downgradeResponsesRequest)(processedBody);
|
|
229
241
|
}
|
|
230
242
|
else {
|
|
231
|
-
result =
|
|
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)(
|
|
249
|
+
result = (0, request_js_4.completionsToClaude)(processedBody);
|
|
238
250
|
break;
|
|
239
251
|
case 'completions->responses':
|
|
240
|
-
result = (0, request_js_5.completionsToResponses)(
|
|
252
|
+
result = (0, request_js_5.completionsToResponses)(processedBody);
|
|
241
253
|
break;
|
|
242
254
|
case 'completions->gemini':
|
|
243
|
-
result = (0, request_js_6.completionsToGemini)(
|
|
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)(
|
|
259
|
+
result = (0, request_js_11.geminiToClaude)(processedBody);
|
|
248
260
|
break;
|
|
249
261
|
case 'gemini->completions':
|
|
250
|
-
result = (0, request_js_12.geminiToCompletions)(
|
|
262
|
+
result = (0, request_js_12.geminiToCompletions)(processedBody);
|
|
251
263
|
break;
|
|
252
264
|
case 'gemini->responses':
|
|
253
|
-
result = (0, request_js_13.geminiToResponsesRequest)(
|
|
265
|
+
result = (0, request_js_13.geminiToResponsesRequest)(processedBody);
|
|
254
266
|
break;
|
|
255
267
|
default:
|
|
256
|
-
result =
|
|
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,
|
|
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 =
|
|
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,
|
|
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 => {
|
package/dist/server/main.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
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
|
|
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
|
|
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.
|
|
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",
|