AIClient-2-API/src/converters/strategies/OpenAIResponsesConverter.js
hex2077 b364341f67 feat(provider): 增强健康检测和自动配置关联功能
- 新增 provider-utils.js 公共模块,提取共用工具函数
- 添加提供商健康检测 API 端点和 UI 按钮
- 实现配置文件自动关联功能(启动时和手动触发)
- 支持 gemini-antigravity 新提供商类型
- 增强健康检测结果记录(时间、模型、错误信息)
- 添加提供商列表分页功能
- 修复 OpenAIResponsesConverter 中 systemMessages 未定义问题
- 更新默认健康检测模型配置
2025-12-10 15:18:42 +08:00

572 lines
21 KiB
JavaScript

/**
* OpenAI Responses API 转换器
* 处理 OpenAI Responses API 格式与其他协议之间的转换
*/
import { BaseConverter } from '../BaseConverter.js';
import { MODEL_PROTOCOL_PREFIX } from '../../common.js';
import {
extractAndProcessSystemMessages as extractSystemMessages,
extractTextFromMessageContent as extractText
} from '../utils.js';
/**
* OpenAI Responses API 转换器类
* 支持 OpenAI Responses 格式与 OpenAI、Claude、Gemini 之间的转换
*/
export class OpenAIResponsesConverter extends BaseConverter {
constructor() {
super(MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES);
}
// =============================================================================
// 请求转换
// =============================================================================
/**
* 转换请求到目标协议
*/
convertRequest(data, toProtocol) {
switch (toProtocol) {
case MODEL_PROTOCOL_PREFIX.OPENAI:
return this.toOpenAIRequest(data);
case MODEL_PROTOCOL_PREFIX.CLAUDE:
return this.toClaudeRequest(data);
case MODEL_PROTOCOL_PREFIX.GEMINI:
return this.toGeminiRequest(data);
default:
throw new Error(`Unsupported target protocol: ${toProtocol}`);
}
}
/**
* 转换响应到目标协议
*/
convertResponse(data, toProtocol, model) {
switch (toProtocol) {
case MODEL_PROTOCOL_PREFIX.OPENAI:
return this.toOpenAIResponse(data, model);
case MODEL_PROTOCOL_PREFIX.CLAUDE:
return this.toClaudeResponse(data, model);
case MODEL_PROTOCOL_PREFIX.GEMINI:
return this.toGeminiResponse(data, model);
default:
throw new Error(`Unsupported target protocol: ${toProtocol}`);
}
}
/**
* 转换流式响应块到目标协议
*/
convertStreamChunk(chunk, toProtocol, model) {
switch (toProtocol) {
case MODEL_PROTOCOL_PREFIX.OPENAI:
return this.toOpenAIStreamChunk(chunk, model);
case MODEL_PROTOCOL_PREFIX.CLAUDE:
return this.toClaudeStreamChunk(chunk, model);
case MODEL_PROTOCOL_PREFIX.GEMINI:
return this.toGeminiStreamChunk(chunk, model);
default:
throw new Error(`Unsupported target protocol: ${toProtocol}`);
}
}
/**
* 转换模型列表到目标协议
*/
convertModelList(data, targetProtocol) {
switch (targetProtocol) {
case MODEL_PROTOCOL_PREFIX.OPENAI:
return this.toOpenAIModelList(data);
case MODEL_PROTOCOL_PREFIX.CLAUDE:
return this.toClaudeModelList(data);
case MODEL_PROTOCOL_PREFIX.GEMINI:
return this.toGeminiModelList(data);
default:
return data;
}
}
// =============================================================================
// 转换到 OpenAI 格式
// =============================================================================
/**
* 将 OpenAI Responses 请求转换为标准 OpenAI 请求
*/
toOpenAIRequest(responsesRequest) {
const openaiRequest = {
model: responsesRequest.model,
messages: [],
stream: responsesRequest.stream || false
};
// OpenAI Responses API 使用 instructions 和 input 字段
// 需要转换为标准的 messages 格式
if (responsesRequest.instructions) {
// instructions 作为系统消息
openaiRequest.messages.push({
role: 'system',
content: responsesRequest.instructions
});
}
// input 包含用户消息和历史对话
if (responsesRequest.input && Array.isArray(responsesRequest.input)) {
responsesRequest.input.forEach(item => {
if (item.type === 'message') {
// 提取消息内容
const content = item.content
.filter(c => c.type === 'input_text')
.map(c => c.text)
.join('\n');
if (content) {
openaiRequest.messages.push({
role: item.role,
content: content
});
}
}
});
}
// 如果有标准的 messages 字段,也支持
if (responsesRequest.messages && Array.isArray(responsesRequest.messages)) {
responsesRequest.messages.forEach(msg => {
openaiRequest.messages.push({
role: msg.role,
content: msg.content
});
});
}
// 复制其他参数
if (responsesRequest.temperature !== undefined) {
openaiRequest.temperature = responsesRequest.temperature;
}
if (responsesRequest.max_tokens !== undefined) {
openaiRequest.max_tokens = responsesRequest.max_tokens;
}
if (responsesRequest.top_p !== undefined) {
openaiRequest.top_p = responsesRequest.top_p;
}
return openaiRequest;
}
/**
* 将 OpenAI Responses 响应转换为标准 OpenAI 响应
*/
toOpenAIResponse(responsesResponse, model) {
// OpenAI Responses 格式已经很接近标准 OpenAI 格式
return {
id: responsesResponse.id || `chatcmpl-${Date.now()}`,
object: 'chat.completion',
created: responsesResponse.created || Math.floor(Date.now() / 1000),
model: model || responsesResponse.model,
choices: responsesResponse.choices || [{
index: 0,
message: {
role: 'assistant',
content: responsesResponse.content || ''
},
finish_reason: responsesResponse.finish_reason || 'stop'
}],
usage: responsesResponse.usage ? {
prompt_tokens: responsesResponse.usage.input_tokens || 0,
completion_tokens: responsesResponse.usage.output_tokens || 0,
total_tokens: responsesResponse.usage.total_tokens || 0,
prompt_tokens_details: {
cached_tokens: responsesResponse.usage.input_tokens_details?.cached_tokens || 0
},
completion_tokens_details: {
reasoning_tokens: responsesResponse.usage.output_tokens_details?.reasoning_tokens || 0
}
} : {
prompt_tokens: 0,
completion_tokens: 0,
total_tokens: 0,
prompt_tokens_details: {
cached_tokens: 0
},
completion_tokens_details: {
reasoning_tokens: 0
}
}
};
}
/**
* 将 OpenAI Responses 流式块转换为标准 OpenAI 流式块
*/
toOpenAIStreamChunk(responsesChunk, model) {
return {
id: responsesChunk.id || `chatcmpl-${Date.now()}`,
object: 'chat.completion.chunk',
created: responsesChunk.created || Math.floor(Date.now() / 1000),
model: model || responsesChunk.model,
choices: responsesChunk.choices || [{
index: 0,
delta: {
content: responsesChunk.delta?.content || ''
},
finish_reason: responsesChunk.finish_reason || null
}]
};
}
// =============================================================================
// 转换到 Claude 格式
// =============================================================================
/**
* 将 OpenAI Responses 请求转换为 Claude 请求
*/
toClaudeRequest(responsesRequest) {
const claudeRequest = {
model: responsesRequest.model,
messages: [],
max_tokens: responsesRequest.max_tokens || 4096,
stream: responsesRequest.stream || false
};
// 处理 instructions 作为系统消息
if (responsesRequest.instructions) {
claudeRequest.system = responsesRequest.instructions;
}
// 处理 input 数组中的消息
if (responsesRequest.input && Array.isArray(responsesRequest.input)) {
responsesRequest.input.forEach(item => {
if (item.type === 'message') {
const content = item.content
.filter(c => c.type === 'input_text')
.map(c => c.text)
.join('\n');
if (content) {
claudeRequest.messages.push({
role: item.role === 'assistant' ? 'assistant' : 'user',
content: content
});
}
}
});
}
// 如果有标准的 messages 字段,也支持
if (responsesRequest.messages && Array.isArray(responsesRequest.messages)) {
const { systemMessages, otherMessages } = extractSystemMessages(
responsesRequest.messages
);
if (!claudeRequest.system && systemMessages.length > 0) {
const systemTexts = systemMessages.map(msg => extractText(msg.content));
claudeRequest.system = systemTexts.join('\n');
}
otherMessages.forEach(msg => {
claudeRequest.messages.push({
role: msg.role === 'assistant' ? 'assistant' : 'user',
content: typeof msg.content === 'string' ? msg.content : extractText(msg.content)
});
});
}
// 复制其他参数
if (responsesRequest.temperature !== undefined) {
claudeRequest.temperature = responsesRequest.temperature;
}
if (responsesRequest.top_p !== undefined) {
claudeRequest.top_p = responsesRequest.top_p;
}
return claudeRequest;
}
/**
* 将 OpenAI Responses 响应转换为 Claude 响应
*/
toClaudeResponse(responsesResponse, model) {
const content = responsesResponse.choices?.[0]?.message?.content ||
responsesResponse.content || '';
return {
id: responsesResponse.id || `msg_${Date.now()}`,
type: 'message',
role: 'assistant',
content: [{
type: 'text',
text: content
}],
model: model || responsesResponse.model,
stop_reason: responsesResponse.choices?.[0]?.finish_reason || 'end_turn',
usage: {
input_tokens: responsesResponse.usage?.input_tokens || responsesResponse.usage?.prompt_tokens || 0,
cache_creation_input_tokens: 0,
cache_read_input_tokens: responsesResponse.usage?.input_tokens_details?.cached_tokens || 0,
output_tokens: responsesResponse.usage?.output_tokens || responsesResponse.usage?.completion_tokens || 0,
prompt_tokens: responsesResponse.usage?.input_tokens || responsesResponse.usage?.prompt_tokens || 0,
completion_tokens: responsesResponse.usage?.output_tokens || responsesResponse.usage?.completion_tokens || 0,
total_tokens: responsesResponse.usage?.total_tokens ||
((responsesResponse.usage?.input_tokens || responsesResponse.usage?.prompt_tokens || 0) +
(responsesResponse.usage?.output_tokens || responsesResponse.usage?.completion_tokens || 0)),
cached_tokens: responsesResponse.usage?.input_tokens_details?.cached_tokens || 0
}
};
}
/**
* 将 OpenAI Responses 流式块转换为 Claude 流式块
*/
toClaudeStreamChunk(responsesChunk, model) {
const delta = responsesChunk.choices?.[0]?.delta || responsesChunk.delta || {};
const finishReason = responsesChunk.choices?.[0]?.finish_reason ||
responsesChunk.finish_reason;
if (finishReason) {
return {
type: 'message_stop'
};
}
if (delta.content) {
return {
type: 'content_block_delta',
index: 0,
delta: {
type: 'text_delta',
text: delta.content
}
};
}
return {
type: 'message_start',
message: {
id: responsesChunk.id || `msg_${Date.now()}`,
type: 'message',
role: 'assistant',
content: [],
model: model || responsesChunk.model
}
};
}
// =============================================================================
// 转换到 Gemini 格式
// =============================================================================
/**
* 将 OpenAI Responses 请求转换为 Gemini 请求
*/
toGeminiRequest(responsesRequest) {
const geminiRequest = {
contents: [],
generationConfig: {}
};
// 处理 instructions 作为系统指令
if (responsesRequest.instructions) {
geminiRequest.systemInstruction = {
parts: [{
text: responsesRequest.instructions
}]
};
}
// 处理 input 数组中的消息
if (responsesRequest.input && Array.isArray(responsesRequest.input)) {
responsesRequest.input.forEach(item => {
if (item.type === 'message') {
const content = item.content
.filter(c => c.type === 'input_text')
.map(c => c.text)
.join('\n');
if (content) {
geminiRequest.contents.push({
role: item.role === 'assistant' ? 'model' : 'user',
parts: [{
text: content
}]
});
}
}
});
}
// 如果有标准的 messages 字段,也支持
if (responsesRequest.messages && Array.isArray(responsesRequest.messages)) {
const { systemMessages, otherMessages } = extractSystemMessages(
responsesRequest.messages
);
if (!geminiRequest.systemInstruction && systemMessages.length > 0) {
const systemTexts = systemMessages.map(msg => extractText(msg.content));
geminiRequest.systemInstruction = {
parts: [{
text: systemTexts.join('\n')
}]
};
}
otherMessages.forEach(msg => {
geminiRequest.contents.push({
role: msg.role === 'assistant' ? 'model' : 'user',
parts: [{
text: typeof msg.content === 'string' ? msg.content : extractText(msg.content)
}]
});
});
}
// 设置生成配置
if (responsesRequest.temperature !== undefined) {
geminiRequest.generationConfig.temperature = responsesRequest.temperature;
}
if (responsesRequest.max_tokens !== undefined) {
geminiRequest.generationConfig.maxOutputTokens = responsesRequest.max_tokens;
}
if (responsesRequest.top_p !== undefined) {
geminiRequest.generationConfig.topP = responsesRequest.top_p;
}
return geminiRequest;
}
/**
* 将 OpenAI Responses 响应转换为 Gemini 响应
*/
toGeminiResponse(responsesResponse, model) {
const content = responsesResponse.choices?.[0]?.message?.content ||
responsesResponse.content || '';
return {
candidates: [{
content: {
parts: [{
text: content
}],
role: 'model'
},
finishReason: this.mapFinishReason(
responsesResponse.choices?.[0]?.finish_reason || 'STOP'
),
index: 0
}],
usageMetadata: {
promptTokenCount: responsesResponse.usage?.input_tokens || responsesResponse.usage?.prompt_tokens || 0,
candidatesTokenCount: responsesResponse.usage?.output_tokens || responsesResponse.usage?.completion_tokens || 0,
totalTokenCount: responsesResponse.usage?.total_tokens ||
((responsesResponse.usage?.input_tokens || responsesResponse.usage?.prompt_tokens || 0) +
(responsesResponse.usage?.output_tokens || responsesResponse.usage?.completion_tokens || 0)),
cachedContentTokenCount: responsesResponse.usage?.input_tokens_details?.cached_tokens || 0,
promptTokensDetails: [{
modality: "TEXT",
tokenCount: responsesResponse.usage?.input_tokens || responsesResponse.usage?.prompt_tokens || 0
}],
candidatesTokensDetails: [{
modality: "TEXT",
tokenCount: responsesResponse.usage?.output_tokens || responsesResponse.usage?.completion_tokens || 0
}],
thoughtsTokenCount: responsesResponse.usage?.output_tokens_details?.reasoning_tokens || 0
}
};
}
/**
* 将 OpenAI Responses 流式块转换为 Gemini 流式块
*/
toGeminiStreamChunk(responsesChunk, model) {
const delta = responsesChunk.choices?.[0]?.delta || responsesChunk.delta || {};
const finishReason = responsesChunk.choices?.[0]?.finish_reason ||
responsesChunk.finish_reason;
return {
candidates: [{
content: {
parts: delta.content ? [{
text: delta.content
}] : [],
role: 'model'
},
finishReason: finishReason ? this.mapFinishReason(finishReason) : null,
index: 0
}]
};
}
// =============================================================================
// 辅助方法
// =============================================================================
/**
* 映射完成原因
*/
mapFinishReason(reason) {
const reasonMap = {
'stop': 'STOP',
'length': 'MAX_TOKENS',
'content_filter': 'SAFETY',
'end_turn': 'STOP'
};
return reasonMap[reason] || 'STOP';
}
/**
* 将 OpenAI Responses 模型列表转换为标准 OpenAI 模型列表
*/
toOpenAIModelList(responsesModels) {
// OpenAI Responses 格式的模型列表已经是标准 OpenAI 格式
// 如果输入已经是标准格式,直接返回
if (responsesModels.object === 'list' && responsesModels.data) {
return responsesModels;
}
// 如果是其他格式,转换为标准格式
return {
object: "list",
data: (responsesModels.models || responsesModels.data || []).map(m => ({
id: m.id || m.name,
object: "model",
created: m.created || Math.floor(Date.now() / 1000),
owned_by: m.owned_by || "openai",
})),
};
}
/**
* 将 OpenAI Responses 模型列表转换为 Claude 模型列表
*/
toClaudeModelList(responsesModels) {
const models = responsesModels.data || responsesModels.models || [];
return {
models: models.map(m => ({
name: m.id || m.name,
description: m.description || "",
})),
};
}
/**
* 将 OpenAI Responses 模型列表转换为 Gemini 模型列表
*/
toGeminiModelList(responsesModels) {
const models = responsesModels.data || responsesModels.models || [];
return {
models: models.map(m => ({
name: `models/${m.id || m.name}`,
version: m.version || "1.0.0",
displayName: m.displayName || m.id || m.name,
description: m.description || `A generative model for text and chat generation. ID: ${m.id || m.name}`,
inputTokenLimit: m.inputTokenLimit || 32768,
outputTokenLimit: m.outputTokenLimit || 8192,
supportedGenerationMethods: m.supportedGenerationMethods || ["generateContent", "streamGenerateContent"]
}))
};
}
}