aicodeswitch 2.0.11 → 2.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/CHANGELOG.md +4 -0
- package/CLAUDE.md +38 -2
- package/TECH.md +292 -170
- package/dist/server/proxy-server.js +88 -13
- package/dist/server/transformers/claude-openai.js +554 -48
- package/dist/server/transformers/streaming.js +169 -2
- package/dist/ui/assets/index-B9EyKK90.js +452 -0
- package/dist/ui/assets/index-Bf2ZEtnh.css +1 -0
- package/dist/ui/index.html +2 -2
- package/package.json +1 -1
- package/schemes/claude.schema.md +945 -0
- package/schemes/openai.schema.md +2162 -0
- package/dist/ui/assets/index-BC_wSFXP.js +0 -452
- package/dist/ui/assets/index-DQ2LJr-O.css +0 -1
|
@@ -1,6 +1,81 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.extractTokenUsageFromClaudeUsage = exports.extractTokenUsageFromOpenAIUsage = exports.transformClaudeResponseToOpenAIChat = exports.transformOpenAIChatResponseToClaude = exports.transformClaudeRequestToOpenAIChat = exports.mapStopReason = exports.convertOpenAIUsageToClaude = void 0;
|
|
3
|
+
exports.normalizeOpenAIStreamEvent = exports.transformResponsesToChatCompletions = exports.transformChatCompletionsToResponses = exports.extractTokenUsageFromClaudeUsage = exports.extractTokenUsageFromOpenAIUsage = exports.transformClaudeResponseToOpenAIChat = exports.transformOpenAIChatResponseToClaude = exports.transformClaudeRequestToOpenAIChat = exports.mapClaudeStopReasonToOpenAI = exports.mapStopReason = exports.convertOpenAIUsageToClaude = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* 将 Claude 图像 content block 转换为 OpenAI 格式
|
|
6
|
+
* Claude: { type: "image", source: { type: "base64", media_type: "image/jpeg", data: "..." } }
|
|
7
|
+
* OpenAI: { type: "image_url", image_url: { url: "data:image/jpeg;base64,..." } }
|
|
8
|
+
*/
|
|
9
|
+
const convertClaudeImageToOpenAI = (block) => {
|
|
10
|
+
if (!block || typeof block !== 'object' || block.type !== 'image') {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
const source = block.source;
|
|
14
|
+
if (!source || typeof source !== 'object') {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
let imageUrl = null;
|
|
18
|
+
// 处理 base64 编码的图像
|
|
19
|
+
if (source.type === 'base64' && source.data && source.media_type) {
|
|
20
|
+
imageUrl = `data:${source.media_type};base64,${source.data}`;
|
|
21
|
+
}
|
|
22
|
+
// 处理 URL 格式的图像
|
|
23
|
+
else if (source.type === 'url' && source.url) {
|
|
24
|
+
imageUrl = source.url;
|
|
25
|
+
}
|
|
26
|
+
// 处理 file_id(如果有的话)
|
|
27
|
+
else if (source.type === 'file' && source.file_id) {
|
|
28
|
+
// file_id 需要特殊处理,这里先保留为占位符
|
|
29
|
+
imageUrl = null; // 需要调用方处理 file_id
|
|
30
|
+
}
|
|
31
|
+
if (!imageUrl) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
type: 'image_url',
|
|
36
|
+
image_url: {
|
|
37
|
+
url: imageUrl,
|
|
38
|
+
detail: 'auto', // 默认使用 auto,可以根据需要调整
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* 将 OpenAI 图像 content block 转换为 Claude 格式
|
|
44
|
+
* OpenAI: { type: "image_url", image_url: { url: "..." } }
|
|
45
|
+
* Claude: { type: "image", source: { type: "base64" | "url", media_type: "...", data/ url: "..." } }
|
|
46
|
+
*/
|
|
47
|
+
const convertOpenAIImageToClaude = (block) => {
|
|
48
|
+
var _a;
|
|
49
|
+
if (!block || typeof block !== 'object' || block.type !== 'image_url') {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
const imageUrl = (_a = block.image_url) === null || _a === void 0 ? void 0 : _a.url;
|
|
53
|
+
if (!imageUrl || typeof imageUrl !== 'string') {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
// 检查是否是 data URL (base64)
|
|
57
|
+
if (imageUrl.startsWith('data:')) {
|
|
58
|
+
const match = imageUrl.match(/^data:([^;]+);base64,(.+)$/);
|
|
59
|
+
if (match) {
|
|
60
|
+
return {
|
|
61
|
+
type: 'image',
|
|
62
|
+
source: {
|
|
63
|
+
type: 'base64',
|
|
64
|
+
media_type: match[1],
|
|
65
|
+
data: match[2],
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// 否则作为 URL 处理
|
|
71
|
+
return {
|
|
72
|
+
type: 'image',
|
|
73
|
+
source: {
|
|
74
|
+
type: 'url',
|
|
75
|
+
url: imageUrl,
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
};
|
|
4
79
|
const toTextContent = (content) => {
|
|
5
80
|
if (typeof content === 'string')
|
|
6
81
|
return content;
|
|
@@ -8,21 +83,52 @@ const toTextContent = (content) => {
|
|
|
8
83
|
return null;
|
|
9
84
|
const parts = [];
|
|
10
85
|
for (const item of content) {
|
|
11
|
-
if (item && typeof item === 'object'
|
|
12
|
-
|
|
86
|
+
if (item && typeof item === 'object') {
|
|
87
|
+
const block = item;
|
|
88
|
+
// 只提取文本内容,忽略图像和其他类型
|
|
89
|
+
if (block.type === 'text' && 'text' in block && typeof block.text === 'string') {
|
|
90
|
+
parts.push(block.text);
|
|
91
|
+
}
|
|
13
92
|
}
|
|
14
93
|
}
|
|
15
94
|
return parts.length > 0 ? parts.join('') : null;
|
|
16
95
|
};
|
|
96
|
+
/**
|
|
97
|
+
* 将 Claude 的 tool_choice 映射到 OpenAI 格式
|
|
98
|
+
* Claude: "auto" | "any" | {type: "tool", name: string}
|
|
99
|
+
* OpenAI: "auto" | "none" | "required" | {type: "function", function: {name: string}}
|
|
100
|
+
*/
|
|
17
101
|
const mapClaudeToolChoiceToOpenAI = (toolChoice) => {
|
|
18
|
-
|
|
102
|
+
var _a;
|
|
103
|
+
// 字符串类型直接映射
|
|
104
|
+
if (toolChoice === 'auto' || toolChoice === 'none') {
|
|
19
105
|
return toolChoice;
|
|
20
106
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
107
|
+
// Claude 的 "any" 映射到 OpenAI 的 "required"
|
|
108
|
+
if (toolChoice === 'any' || toolChoice === 'required') {
|
|
109
|
+
return 'required';
|
|
110
|
+
}
|
|
111
|
+
// 对象类型:{type: "tool", name: "tool_name"} -> {type: "function", function: {name: "tool_name"}}
|
|
112
|
+
if (toolChoice && typeof toolChoice === 'object') {
|
|
113
|
+
const tc = toolChoice;
|
|
114
|
+
// Claude 格式
|
|
115
|
+
if (tc.type === 'tool' && tc.name) {
|
|
116
|
+
return {
|
|
117
|
+
type: 'function',
|
|
118
|
+
function: { name: tc.name },
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
// OpenAI 格式(已经是正确格式)
|
|
122
|
+
if (tc.type === 'function' && ((_a = tc.function) === null || _a === void 0 ? void 0 : _a.name)) {
|
|
123
|
+
return toolChoice;
|
|
124
|
+
}
|
|
125
|
+
// 兼容旧的 name 字段格式
|
|
126
|
+
if (tc.name && !tc.type) {
|
|
127
|
+
return {
|
|
128
|
+
type: 'function',
|
|
129
|
+
function: { name: tc.name },
|
|
130
|
+
};
|
|
131
|
+
}
|
|
26
132
|
}
|
|
27
133
|
return toolChoice;
|
|
28
134
|
};
|
|
@@ -36,6 +142,11 @@ const convertOpenAIUsageToClaude = (usage) => {
|
|
|
36
142
|
};
|
|
37
143
|
};
|
|
38
144
|
exports.convertOpenAIUsageToClaude = convertOpenAIUsageToClaude;
|
|
145
|
+
/**
|
|
146
|
+
* 将 OpenAI 的 finish_reason 映射到 Claude 的 stop_reason
|
|
147
|
+
* OpenAI: "stop" | "length" | "tool_calls" | "content_filter"
|
|
148
|
+
* Claude: "end_turn" | "max_tokens" | "tool_use" | "stop_sequence" | "max_thinking_length"
|
|
149
|
+
*/
|
|
39
150
|
const mapStopReason = (finishReason) => {
|
|
40
151
|
switch (finishReason) {
|
|
41
152
|
case 'stop':
|
|
@@ -51,6 +162,29 @@ const mapStopReason = (finishReason) => {
|
|
|
51
162
|
}
|
|
52
163
|
};
|
|
53
164
|
exports.mapStopReason = mapStopReason;
|
|
165
|
+
/**
|
|
166
|
+
* 将 Claude 的 stop_reason 映射到 OpenAI 的 finish_reason
|
|
167
|
+
* Claude: "end_turn" | "max_tokens" | "tool_use" | "stop_sequence" | "max_thinking_length"
|
|
168
|
+
* OpenAI: "stop" | "length" | "tool_calls" | "content_filter"
|
|
169
|
+
*/
|
|
170
|
+
const mapClaudeStopReasonToOpenAI = (stopReason) => {
|
|
171
|
+
switch (stopReason) {
|
|
172
|
+
case 'end_turn':
|
|
173
|
+
return 'stop';
|
|
174
|
+
case 'max_tokens':
|
|
175
|
+
case 'max_thinking_length': // Claude 的思考预算用完,映射为 length
|
|
176
|
+
return 'length';
|
|
177
|
+
case 'tool_use':
|
|
178
|
+
return 'tool_calls';
|
|
179
|
+
case 'stop_sequence':
|
|
180
|
+
return 'stop';
|
|
181
|
+
case 'content_filter':
|
|
182
|
+
return 'content_filter';
|
|
183
|
+
default:
|
|
184
|
+
return 'stop';
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
exports.mapClaudeStopReasonToOpenAI = mapClaudeStopReasonToOpenAI;
|
|
54
188
|
/**
|
|
55
189
|
* 检查模型是否需要使用 developer 角色而不是 system 角色
|
|
56
190
|
* 某些 OpenAI 兼容的 API (如 DeepSeek) 不支持 system 角色,需要使用 developer
|
|
@@ -128,14 +262,38 @@ const ensureLastMessageIsUser = (messages) => {
|
|
|
128
262
|
});
|
|
129
263
|
};
|
|
130
264
|
const transformClaudeRequestToOpenAIChat = (body, targetModel) => {
|
|
131
|
-
var _a;
|
|
265
|
+
var _a, _b, _c;
|
|
132
266
|
const messages = [];
|
|
133
267
|
const useDeveloperRole = shouldUseDeveloperRole(targetModel);
|
|
134
268
|
const systemRoleName = useDeveloperRole ? 'developer' : 'system';
|
|
135
269
|
if (body.system) {
|
|
136
|
-
|
|
137
|
-
if (
|
|
138
|
-
messages.push({ role: systemRoleName, content:
|
|
270
|
+
// 处理 system 字段:字符串或数组
|
|
271
|
+
if (typeof body.system === 'string') {
|
|
272
|
+
messages.push({ role: systemRoleName, content: body.system });
|
|
273
|
+
}
|
|
274
|
+
else if (Array.isArray(body.system)) {
|
|
275
|
+
// system 是数组,提取文本内容
|
|
276
|
+
const systemTexts = [];
|
|
277
|
+
for (const block of body.system) {
|
|
278
|
+
if (block && typeof block === 'object') {
|
|
279
|
+
const blk = block;
|
|
280
|
+
if (blk.type === 'text' && typeof blk.text === 'string') {
|
|
281
|
+
systemTexts.push(blk.text);
|
|
282
|
+
}
|
|
283
|
+
// 注意:OpenAI 的 system 角色不支持图像,忽略图像块
|
|
284
|
+
// 缓存控制块也忽略(OpenAI 不支持)
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
if (systemTexts.length > 0) {
|
|
288
|
+
messages.push({ role: systemRoleName, content: systemTexts.join('\n\n') });
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
else if (typeof body.system === 'object') {
|
|
292
|
+
// 单个 system block
|
|
293
|
+
const text = toTextContent([body.system]);
|
|
294
|
+
if (text) {
|
|
295
|
+
messages.push({ role: systemRoleName, content: text });
|
|
296
|
+
}
|
|
139
297
|
}
|
|
140
298
|
}
|
|
141
299
|
if (Array.isArray(body.messages)) {
|
|
@@ -150,14 +308,30 @@ const transformClaudeRequestToOpenAIChat = (body, targetModel) => {
|
|
|
150
308
|
}
|
|
151
309
|
if (Array.isArray(message.content)) {
|
|
152
310
|
const textParts = [];
|
|
311
|
+
const imageParts = []; // OpenAI 格式的图像内容
|
|
153
312
|
const toolCalls = [];
|
|
154
313
|
const toolResultMessages = [];
|
|
314
|
+
const thinkingParts = [];
|
|
155
315
|
for (const block of message.content) {
|
|
156
316
|
if (block && typeof block === 'object') {
|
|
157
|
-
|
|
317
|
+
const blockType = block.type;
|
|
318
|
+
// 处理文本内容
|
|
319
|
+
if (blockType === 'text' && typeof block.text === 'string') {
|
|
158
320
|
textParts.push(block.text);
|
|
159
321
|
}
|
|
160
|
-
|
|
322
|
+
// 处理图像内容 - 转换为 OpenAI 格式
|
|
323
|
+
if (blockType === 'image') {
|
|
324
|
+
const openaiImage = convertClaudeImageToOpenAI(block);
|
|
325
|
+
if (openaiImage) {
|
|
326
|
+
imageParts.push(openaiImage);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
// 处理 thinking content block(转换为文本,因为 OpenAI Chat 不直接支持)
|
|
330
|
+
if (blockType === 'thinking' && typeof block.thinking === 'string') {
|
|
331
|
+
thinkingParts.push(block.thinking);
|
|
332
|
+
}
|
|
333
|
+
// 处理工具使用
|
|
334
|
+
if (blockType === 'tool_use') {
|
|
161
335
|
const toolId = block.id || `tool_${toolCalls.length + 1}`;
|
|
162
336
|
const toolName = block.name || 'tool';
|
|
163
337
|
const input = (_a = block.input) !== null && _a !== void 0 ? _a : {};
|
|
@@ -170,23 +344,56 @@ const transformClaudeRequestToOpenAIChat = (body, targetModel) => {
|
|
|
170
344
|
},
|
|
171
345
|
});
|
|
172
346
|
}
|
|
173
|
-
|
|
347
|
+
// 处理工具结果
|
|
348
|
+
if (blockType === 'tool_result') {
|
|
174
349
|
const toolCallId = block.tool_use_id || block.id;
|
|
175
350
|
const toolContent = block.content;
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
tool_call_id: toolCallId,
|
|
179
|
-
content: typeof toolContent === 'string' ? toolContent : JSON.stringify(toolContent !== null && toolContent !== void 0 ? toolContent : {}),
|
|
180
|
-
});
|
|
351
|
+
const isError = block.is_error;
|
|
352
|
+
toolResultMessages.push(Object.assign({ role: 'tool', tool_call_id: toolCallId, content: typeof toolContent === 'string' ? toolContent : JSON.stringify(toolContent !== null && toolContent !== void 0 ? toolContent : {}) }, (isError !== undefined && { is_error: isError })));
|
|
181
353
|
}
|
|
182
354
|
}
|
|
183
355
|
}
|
|
184
|
-
//
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
356
|
+
// 构建消息内容
|
|
357
|
+
// 如果有图像,content 必须是数组格式;否则可以是字符串
|
|
358
|
+
let openaiMessage;
|
|
359
|
+
if (imageParts.length > 0) {
|
|
360
|
+
// 有图像内容,使用数组格式
|
|
361
|
+
const contentArray = [];
|
|
362
|
+
// 添加文本部分(如果有)
|
|
363
|
+
if (textParts.length > 0) {
|
|
364
|
+
contentArray.push({
|
|
365
|
+
type: 'text',
|
|
366
|
+
text: textParts.join(''),
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
// 添加图像部分
|
|
370
|
+
contentArray.push(...imageParts);
|
|
371
|
+
// 添加 thinking 内容(如果有)
|
|
372
|
+
if (thinkingParts.length > 0) {
|
|
373
|
+
const thinkingText = thinkingParts.join('\n');
|
|
374
|
+
contentArray.push({
|
|
375
|
+
type: 'text',
|
|
376
|
+
text: `<thinking>\n${thinkingText}\n</thinking>`,
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
openaiMessage = {
|
|
380
|
+
role: mappedRole,
|
|
381
|
+
content: contentArray,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
else {
|
|
385
|
+
// 没有图像,使用字符串格式(更简单)
|
|
386
|
+
let content = textParts.length > 0 ? textParts.join('') : '';
|
|
387
|
+
// 如果有 thinking 内容,将其作为前缀添加到文本中(用特殊标记包裹)
|
|
388
|
+
if (thinkingParts.length > 0) {
|
|
389
|
+
const thinkingText = thinkingParts.join('\n');
|
|
390
|
+
content = `<thinking>\n${thinkingText}\n</thinking>\n${content}`;
|
|
391
|
+
}
|
|
392
|
+
openaiMessage = {
|
|
393
|
+
role: mappedRole,
|
|
394
|
+
content: content || '', // 确保不为 undefined
|
|
395
|
+
};
|
|
396
|
+
}
|
|
190
397
|
if (toolCalls.length > 0) {
|
|
191
398
|
openaiMessage.tool_calls = toolCalls;
|
|
192
399
|
}
|
|
@@ -227,40 +434,131 @@ const transformClaudeRequestToOpenAIChat = (body, targetModel) => {
|
|
|
227
434
|
openaiBody.stream = true;
|
|
228
435
|
openaiBody.stream_options = { include_usage: true };
|
|
229
436
|
}
|
|
437
|
+
// 处理 thinking/reasoning 配置的转换
|
|
438
|
+
// Claude: thinking: { type: "enabled" | "disabled" | "auto", budget_tokens?: number }
|
|
439
|
+
// OpenAI Chat: thinking: { type: "enabled" | "disabled" | "auto" }
|
|
440
|
+
// OpenAI Responses: thinking + reasoning (effort)
|
|
441
|
+
// DeepSeek: thinking: { type: "enabled" | "disabled" | "auto" }
|
|
442
|
+
if (body.thinking && typeof body.thinking === 'object') {
|
|
443
|
+
const claudeThinking = body.thinking;
|
|
444
|
+
// 为所有 OpenAI 兼容 API 添加 thinking 配置
|
|
445
|
+
if (claudeThinking.type) {
|
|
446
|
+
openaiBody.thinking = { type: claudeThinking.type };
|
|
447
|
+
}
|
|
448
|
+
// 为 OpenAI Responses API 添加 reasoning 配置
|
|
449
|
+
// 映射关系:enabled->medium, disabled->minimal, auto->low
|
|
450
|
+
if (claudeThinking.type) {
|
|
451
|
+
const effortMap = {
|
|
452
|
+
'enabled': 'medium',
|
|
453
|
+
'disabled': 'minimal',
|
|
454
|
+
'auto': 'low'
|
|
455
|
+
};
|
|
456
|
+
openaiBody.reasoning = {
|
|
457
|
+
effort: (effortMap[claudeThinking.type] || 'medium')
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
// 处理直接的 reasoning_effort 字段(来自请求体)
|
|
462
|
+
if (body.reasoning_effort || ((_b = body.reasoning) === null || _b === void 0 ? void 0 : _b.effort)) {
|
|
463
|
+
const effort = body.reasoning_effort || ((_c = body.reasoning) === null || _c === void 0 ? void 0 : _c.effort);
|
|
464
|
+
if (typeof effort === 'string') {
|
|
465
|
+
openaiBody.reasoning = {
|
|
466
|
+
effort: effort
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
}
|
|
230
470
|
return openaiBody;
|
|
231
471
|
};
|
|
232
472
|
exports.transformClaudeRequestToOpenAIChat = transformClaudeRequestToOpenAIChat;
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
const
|
|
473
|
+
/**
|
|
474
|
+
* 从 OpenAI 消息内容中提取文本和图像
|
|
475
|
+
* 支持字符串格式和数组格式
|
|
476
|
+
*/
|
|
477
|
+
const extractOpenAIContent = (content) => {
|
|
478
|
+
const result = { text: '', images: [] };
|
|
479
|
+
if (typeof content === 'string') {
|
|
480
|
+
result.text = content;
|
|
481
|
+
return result;
|
|
482
|
+
}
|
|
483
|
+
if (!Array.isArray(content)) {
|
|
484
|
+
return result;
|
|
485
|
+
}
|
|
239
486
|
for (const item of content) {
|
|
240
|
-
if (item && typeof item === 'object'
|
|
241
|
-
|
|
487
|
+
if (item && typeof item === 'object') {
|
|
488
|
+
const block = item;
|
|
489
|
+
// 提取文本内容
|
|
490
|
+
if (block.type === 'text' && typeof block.text === 'string') {
|
|
491
|
+
result.text += block.text;
|
|
492
|
+
}
|
|
493
|
+
// 提取图像内容
|
|
494
|
+
if (block.type === 'image_url') {
|
|
495
|
+
const claudeImage = convertOpenAIImageToClaude(block);
|
|
496
|
+
if (claudeImage) {
|
|
497
|
+
result.images.push(claudeImage);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
242
500
|
}
|
|
243
501
|
}
|
|
244
|
-
return
|
|
502
|
+
return result;
|
|
245
503
|
};
|
|
246
504
|
const transformOpenAIChatResponseToClaude = (body) => {
|
|
247
|
-
var _a, _b;
|
|
505
|
+
var _a, _b, _c;
|
|
248
506
|
const choice = Array.isArray(body === null || body === void 0 ? void 0 : body.choices) ? body.choices[0] : null;
|
|
249
507
|
const message = (choice === null || choice === void 0 ? void 0 : choice.message) || {};
|
|
250
508
|
const contentBlocks = [];
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
509
|
+
// 提取文本和图像内容
|
|
510
|
+
const extractedContent = extractOpenAIContent(message.content);
|
|
511
|
+
// 添加图像内容块
|
|
512
|
+
for (const image of extractedContent.images) {
|
|
513
|
+
contentBlocks.push(image);
|
|
514
|
+
}
|
|
515
|
+
// 添加文本内容块
|
|
516
|
+
if (extractedContent.text) {
|
|
517
|
+
contentBlocks.push({ type: 'text', text: extractedContent.text });
|
|
518
|
+
}
|
|
519
|
+
// 处理 thinking 内容(如果 OpenAI 返回了独立的 thinking 字段)
|
|
520
|
+
// OpenAI Chat Completions API 可能在 message 中包含 thinking
|
|
521
|
+
if (message.thinking && typeof message.thinking === 'string') {
|
|
522
|
+
contentBlocks.unshift({ type: 'thinking', thinking: message.thinking });
|
|
523
|
+
}
|
|
524
|
+
else if (message.thinking_content) {
|
|
525
|
+
contentBlocks.unshift({ type: 'thinking', thinking: message.thinking_content });
|
|
526
|
+
}
|
|
527
|
+
// 处理 OpenAI Responses API 的 reasoning.summary 和 reasoning content
|
|
528
|
+
// Responses API 可能在 output 数组中包含 reasoning 内容
|
|
529
|
+
if (Array.isArray(body === null || body === void 0 ? void 0 : body.output)) {
|
|
530
|
+
for (const outputItem of body.output) {
|
|
531
|
+
// 处理 reasoning summary
|
|
532
|
+
if (outputItem.type === 'reasoning' && outputItem.content) {
|
|
533
|
+
for (const part of outputItem.content) {
|
|
534
|
+
if (part.type === 'summary_text' && part.text) {
|
|
535
|
+
contentBlocks.unshift({ type: 'thinking', thinking: part.text });
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
// 处理 message 中的 thinking/reasoning
|
|
540
|
+
if (outputItem.type === 'message' && Array.isArray(outputItem.content)) {
|
|
541
|
+
for (const part of outputItem.content) {
|
|
542
|
+
if (part.type === 'thinking' && part.text) {
|
|
543
|
+
contentBlocks.unshift({ type: 'thinking', thinking: part.text });
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
// 处理 reasoning.summary 字段(OpenAI Responses API)
|
|
550
|
+
if (((_a = body === null || body === void 0 ? void 0 : body.reasoning) === null || _a === void 0 ? void 0 : _a.summary) && typeof body.reasoning.summary === 'string') {
|
|
551
|
+
contentBlocks.unshift({ type: 'thinking', thinking: body.reasoning.summary });
|
|
254
552
|
}
|
|
255
553
|
if (Array.isArray(message.tool_calls)) {
|
|
256
554
|
for (const toolCall of message.tool_calls) {
|
|
257
|
-
const toolName = ((
|
|
555
|
+
const toolName = ((_b = toolCall === null || toolCall === void 0 ? void 0 : toolCall.function) === null || _b === void 0 ? void 0 : _b.name) || 'tool';
|
|
258
556
|
let input = {};
|
|
259
|
-
if ((
|
|
557
|
+
if ((_c = toolCall === null || toolCall === void 0 ? void 0 : toolCall.function) === null || _c === void 0 ? void 0 : _c.arguments) {
|
|
260
558
|
try {
|
|
261
559
|
input = JSON.parse(toolCall.function.arguments);
|
|
262
560
|
}
|
|
263
|
-
catch (
|
|
561
|
+
catch (_d) {
|
|
264
562
|
input = toolCall.function.arguments;
|
|
265
563
|
}
|
|
266
564
|
}
|
|
@@ -292,11 +590,23 @@ exports.transformOpenAIChatResponseToClaude = transformOpenAIChatResponseToClaud
|
|
|
292
590
|
const transformClaudeResponseToOpenAIChat = (body) => {
|
|
293
591
|
const content = (body === null || body === void 0 ? void 0 : body.content) || [];
|
|
294
592
|
let textContent = '';
|
|
593
|
+
const imageContents = []; // OpenAI 格式的图像
|
|
295
594
|
const toolCalls = [];
|
|
595
|
+
let thinkingContent = '';
|
|
296
596
|
for (const block of content) {
|
|
297
597
|
if ((block === null || block === void 0 ? void 0 : block.type) === 'text') {
|
|
298
598
|
textContent += block.text || '';
|
|
299
599
|
}
|
|
600
|
+
else if ((block === null || block === void 0 ? void 0 : block.type) === 'image') {
|
|
601
|
+
// 转换 Claude 图像为 OpenAI 格式
|
|
602
|
+
const openaiImage = convertClaudeImageToOpenAI(block);
|
|
603
|
+
if (openaiImage) {
|
|
604
|
+
imageContents.push(openaiImage);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
else if ((block === null || block === void 0 ? void 0 : block.type) === 'thinking') {
|
|
608
|
+
thinkingContent += block.thinking || '';
|
|
609
|
+
}
|
|
300
610
|
else if ((block === null || block === void 0 ? void 0 : block.type) === 'tool_use') {
|
|
301
611
|
toolCalls.push({
|
|
302
612
|
id: block.id,
|
|
@@ -308,10 +618,37 @@ const transformClaudeResponseToOpenAIChat = (body) => {
|
|
|
308
618
|
});
|
|
309
619
|
}
|
|
310
620
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
621
|
+
// 构建消息内容
|
|
622
|
+
// 如果有图像,使用数组格式;否则使用字符串格式
|
|
623
|
+
let message;
|
|
624
|
+
if (imageContents.length > 0) {
|
|
625
|
+
// 有图像,使用数组格式
|
|
626
|
+
const contentArray = [];
|
|
627
|
+
// 添加文本
|
|
628
|
+
if (textContent) {
|
|
629
|
+
contentArray.push({
|
|
630
|
+
type: 'text',
|
|
631
|
+
text: textContent,
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
// 添加图像
|
|
635
|
+
contentArray.push(...imageContents);
|
|
636
|
+
message = {
|
|
637
|
+
role: 'assistant',
|
|
638
|
+
content: contentArray,
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
else {
|
|
642
|
+
// 没有图像,使用字符串格式
|
|
643
|
+
message = {
|
|
644
|
+
role: 'assistant',
|
|
645
|
+
content: textContent,
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
// 如果有 thinking 内容,添加到消息中
|
|
649
|
+
if (thinkingContent) {
|
|
650
|
+
message.thinking = thinkingContent;
|
|
651
|
+
}
|
|
315
652
|
if (toolCalls.length > 0) {
|
|
316
653
|
message.tool_calls = toolCalls;
|
|
317
654
|
}
|
|
@@ -328,7 +665,7 @@ const transformClaudeResponseToOpenAIChat = (body) => {
|
|
|
328
665
|
choices: [{
|
|
329
666
|
index: 0,
|
|
330
667
|
message,
|
|
331
|
-
finish_reason: (0, exports.
|
|
668
|
+
finish_reason: (0, exports.mapClaudeStopReasonToOpenAI)(body === null || body === void 0 ? void 0 : body.stop_reason),
|
|
332
669
|
}],
|
|
333
670
|
usage,
|
|
334
671
|
};
|
|
@@ -360,3 +697,172 @@ const extractTokenUsageFromClaudeUsage = (usage) => {
|
|
|
360
697
|
};
|
|
361
698
|
};
|
|
362
699
|
exports.extractTokenUsageFromClaudeUsage = extractTokenUsageFromClaudeUsage;
|
|
700
|
+
// ============================================================================
|
|
701
|
+
// OpenAI Chat Completions API ↔ OpenAI Responses API 转换
|
|
702
|
+
// ============================================================================
|
|
703
|
+
/**
|
|
704
|
+
* 将 OpenAI Chat Completions 请求转换为 OpenAI Responses API 请求
|
|
705
|
+
* Chat Completions: {model, messages, tools, temperature, ...}
|
|
706
|
+
* Responses: {model, input, instructions, tools, temperature, ...}
|
|
707
|
+
*/
|
|
708
|
+
const transformChatCompletionsToResponses = (body) => {
|
|
709
|
+
const responsesBody = {
|
|
710
|
+
model: body.model,
|
|
711
|
+
};
|
|
712
|
+
// 转换 messages -> input
|
|
713
|
+
if (Array.isArray(body.messages) && body.messages.length > 0) {
|
|
714
|
+
// 提取最后一条用户消息作为 input
|
|
715
|
+
const lastUserMessage = [...body.messages].reverse().find(m => m.role === 'user');
|
|
716
|
+
if (lastUserMessage) {
|
|
717
|
+
// 处理 content 格式
|
|
718
|
+
if (typeof lastUserMessage.content === 'string') {
|
|
719
|
+
responsesBody.input = lastUserMessage.content;
|
|
720
|
+
}
|
|
721
|
+
else if (Array.isArray(lastUserMessage.content)) {
|
|
722
|
+
// 保留数组格式(支持图像等)
|
|
723
|
+
responsesBody.input = lastUserMessage.content;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
// 提取 system 消息作为 instructions
|
|
727
|
+
const systemMessage = body.messages.find((m) => m.role === 'system' || m.role === 'developer');
|
|
728
|
+
if (systemMessage && typeof systemMessage.content === 'string') {
|
|
729
|
+
responsesBody.instructions = systemMessage.content;
|
|
730
|
+
}
|
|
731
|
+
// 如果有对话历史,可以考虑设置 previous_response_id(需要从之前的响应中获取)
|
|
732
|
+
// 这里暂时不实现,因为需要维护对话状态
|
|
733
|
+
}
|
|
734
|
+
// 转换参数
|
|
735
|
+
if (typeof body.temperature === 'number') {
|
|
736
|
+
responsesBody.temperature = body.temperature;
|
|
737
|
+
}
|
|
738
|
+
if (typeof body.top_p === 'number') {
|
|
739
|
+
responsesBody.top_p = body.top_p;
|
|
740
|
+
}
|
|
741
|
+
if (typeof body.max_tokens === 'number') {
|
|
742
|
+
responsesBody.max_output_tokens = body.max_tokens;
|
|
743
|
+
}
|
|
744
|
+
// 转换 tools
|
|
745
|
+
if (Array.isArray(body.tools)) {
|
|
746
|
+
responsesBody.tools = body.tools;
|
|
747
|
+
}
|
|
748
|
+
if (body.tool_choice) {
|
|
749
|
+
responsesBody.tool_choice = body.tool_choice;
|
|
750
|
+
}
|
|
751
|
+
// 转换流式选项
|
|
752
|
+
if (body.stream === true) {
|
|
753
|
+
responsesBody.stream = true;
|
|
754
|
+
}
|
|
755
|
+
// 转换 reasoning 配置
|
|
756
|
+
if (body.reasoning && typeof body.reasoning === 'object') {
|
|
757
|
+
responsesBody.reasoning = body.reasoning;
|
|
758
|
+
}
|
|
759
|
+
// 转换其他配置
|
|
760
|
+
if (body.metadata) {
|
|
761
|
+
responsesBody.metadata = body.metadata;
|
|
762
|
+
}
|
|
763
|
+
return responsesBody;
|
|
764
|
+
};
|
|
765
|
+
exports.transformChatCompletionsToResponses = transformChatCompletionsToResponses;
|
|
766
|
+
/**
|
|
767
|
+
* 将 OpenAI Responses API 响应转换为 Chat Completions 格式
|
|
768
|
+
* Responses: {id, object: "response", output: [{type: "message", content: [...]}], usage, ...}
|
|
769
|
+
* Chat Completions: {id, object: "chat.completion", choices: [{message: {content, ...}}], usage, ...}
|
|
770
|
+
*/
|
|
771
|
+
const transformResponsesToChatCompletions = (body) => {
|
|
772
|
+
var _a;
|
|
773
|
+
if (!body || typeof body !== 'object') {
|
|
774
|
+
return body;
|
|
775
|
+
}
|
|
776
|
+
// 提取消息内容
|
|
777
|
+
let textContent = '';
|
|
778
|
+
const thinkingContent = [];
|
|
779
|
+
const toolCalls = [];
|
|
780
|
+
// 遍历 output 数组
|
|
781
|
+
if (Array.isArray(body.output)) {
|
|
782
|
+
for (const outputItem of body.output) {
|
|
783
|
+
if (outputItem.type === 'message' && Array.isArray(outputItem.content)) {
|
|
784
|
+
for (const part of outputItem.content) {
|
|
785
|
+
// 处理文本输出
|
|
786
|
+
if (part.type === 'output_text' && typeof part.text === 'string') {
|
|
787
|
+
textContent += part.text;
|
|
788
|
+
}
|
|
789
|
+
// 处理思考内容
|
|
790
|
+
if (part.type === 'thinking' && typeof part.text === 'string') {
|
|
791
|
+
thinkingContent.push(part.text);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
// 处理 reasoning summary
|
|
796
|
+
if (outputItem.type === 'reasoning' && Array.isArray(outputItem.content)) {
|
|
797
|
+
for (const part of outputItem.content) {
|
|
798
|
+
if (part.type === 'summary_text' && typeof part.text === 'string') {
|
|
799
|
+
thinkingContent.push(part.text);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
// 处理工具调用(如果有)
|
|
804
|
+
if (outputItem.type === 'function_call') {
|
|
805
|
+
toolCalls.push({
|
|
806
|
+
id: outputItem.id || `call_${toolCalls.length}`,
|
|
807
|
+
type: 'function',
|
|
808
|
+
function: {
|
|
809
|
+
name: outputItem.name || 'unknown',
|
|
810
|
+
arguments: outputItem.arguments || '{}',
|
|
811
|
+
},
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
// 构建消息对象
|
|
817
|
+
const message = {
|
|
818
|
+
role: 'assistant',
|
|
819
|
+
content: textContent,
|
|
820
|
+
};
|
|
821
|
+
// 添加 thinking 内容
|
|
822
|
+
if (thinkingContent.length > 0) {
|
|
823
|
+
message.thinking = thinkingContent.join('\n\n');
|
|
824
|
+
}
|
|
825
|
+
// 添加工具调用
|
|
826
|
+
if (toolCalls.length > 0) {
|
|
827
|
+
message.tool_calls = toolCalls;
|
|
828
|
+
}
|
|
829
|
+
// 转换 usage
|
|
830
|
+
const usage = body.usage ? {
|
|
831
|
+
prompt_tokens: body.usage.input_tokens || 0,
|
|
832
|
+
completion_tokens: body.usage.output_tokens || 0,
|
|
833
|
+
total_tokens: (body.usage.input_tokens || 0) + (body.usage.output_tokens || 0),
|
|
834
|
+
} : undefined;
|
|
835
|
+
// 转换 finish_reason
|
|
836
|
+
let finish_reason = 'stop';
|
|
837
|
+
if (body.status === 'incomplete') {
|
|
838
|
+
finish_reason = ((_a = body.incomplete_details) === null || _a === void 0 ? void 0 : _a.reason) === 'max_tokens' ? 'length' : 'stop';
|
|
839
|
+
}
|
|
840
|
+
return {
|
|
841
|
+
id: body.id,
|
|
842
|
+
object: 'chat.completion',
|
|
843
|
+
created: body.created_at || Math.floor(Date.now() / 1000),
|
|
844
|
+
model: body.model,
|
|
845
|
+
choices: [{
|
|
846
|
+
index: 0,
|
|
847
|
+
message,
|
|
848
|
+
finish_reason,
|
|
849
|
+
}],
|
|
850
|
+
usage,
|
|
851
|
+
};
|
|
852
|
+
};
|
|
853
|
+
exports.transformResponsesToChatCompletions = transformResponsesToChatCompletions;
|
|
854
|
+
/**
|
|
855
|
+
* 将 OpenAI Chat Completions 流式事件转换为 Responses API 流式事件格式
|
|
856
|
+
* 这主要用于解析不同格式的流式响应
|
|
857
|
+
*/
|
|
858
|
+
const normalizeOpenAIStreamEvent = (event) => {
|
|
859
|
+
const type = event.event;
|
|
860
|
+
// 如果是 Responses API 事件,直接返回
|
|
861
|
+
if (type && type.startsWith('response.')) {
|
|
862
|
+
return event;
|
|
863
|
+
}
|
|
864
|
+
// Chat Completions API 事件
|
|
865
|
+
// 实际的转换在 streaming.ts 的转换器中处理
|
|
866
|
+
return event;
|
|
867
|
+
};
|
|
868
|
+
exports.normalizeOpenAIStreamEvent = normalizeOpenAIStreamEvent;
|