- 新增 provider-utils.js 公共模块,提取共用工具函数 - 添加提供商健康检测 API 端点和 UI 按钮 - 实现配置文件自动关联功能(启动时和手动触发) - 支持 gemini-antigravity 新提供商类型 - 增强健康检测结果记录(时间、模型、错误信息) - 添加提供商列表分页功能 - 修复 OpenAIResponsesConverter 中 systemMessages 未定义问题 - 更新默认健康检测模型配置
572 lines
21 KiB
JavaScript
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"]
|
|
}))
|
|
};
|
|
}
|
|
|
|
}
|
|
|