feat: 增强模型支持和修复流处理问题
- 添加新的Qwen模型(coder-model, vision-model)到提供者列表 - 修复OpenAI Responses流结束事件处理,避免下游类型校验错误 - 更新Qwen API端点地址和版本号 - 重构Codex转换器,分离OpenAI和OpenAI Responses的转换逻辑 - 优化工具调用处理,支持嵌套function结构 - 移除健康检查功能,简化API管理初始化 - 修复消息角色转换(developer→assistant)和类型标记
This commit is contained in:
parent
ce8e8ad855
commit
6ee7e78c90
9 changed files with 338 additions and 362 deletions
|
|
@ -29,11 +29,6 @@ export class CodexConverter extends BaseConverter {
|
||||||
* 转换请求
|
* 转换请求
|
||||||
*/
|
*/
|
||||||
convertRequest(data, targetProtocol) {
|
convertRequest(data, targetProtocol) {
|
||||||
if (targetProtocol === MODEL_PROTOCOL_PREFIX.CODEX) {
|
|
||||||
return this.toCodexRequest(data);
|
|
||||||
} else if (targetProtocol === MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES) {
|
|
||||||
return this.toOpenAIResponsesRequest(data);
|
|
||||||
}
|
|
||||||
throw new Error(`Unsupported target protocol: ${targetProtocol}`);
|
throw new Error(`Unsupported target protocol: ${targetProtocol}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -57,14 +52,6 @@ export class CodexConverter extends BaseConverter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* OpenAI Responses → Codex 请求转换 (或处理已经转换好的数据)
|
|
||||||
*/
|
|
||||||
toOpenAIResponsesRequest(data) {
|
|
||||||
// 如果输入已经是 OpenAI Responses 格式,将其转换为 Codex 格式
|
|
||||||
return this.toCodexRequest(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 转换流式响应块
|
* 转换流式响应块
|
||||||
*/
|
*/
|
||||||
|
|
@ -78,15 +65,85 @@ export class CodexConverter extends BaseConverter {
|
||||||
return this.toGeminiStreamChunk(chunk, model);
|
return this.toGeminiStreamChunk(chunk, model);
|
||||||
case MODEL_PROTOCOL_PREFIX.CLAUDE:
|
case MODEL_PROTOCOL_PREFIX.CLAUDE:
|
||||||
return this.toClaudeStreamChunk(chunk, model);
|
return this.toClaudeStreamChunk(chunk, model);
|
||||||
|
case MODEL_PROTOCOL_PREFIX.CODEX:
|
||||||
|
return chunk; // Codex to Codex
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unsupported target protocol: ${targetProtocol}`);
|
throw new Error(`Unsupported target protocol: ${targetProtocol}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 转换模型列表
|
||||||
|
*/
|
||||||
|
convertModelList(data, targetProtocol) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OpenAI Responses → Codex 请求转换
|
||||||
|
*/
|
||||||
|
toOpenAIResponsesToCodexRequest(responsesRequest) {
|
||||||
|
let codexRequest = { ...responsesRequest };
|
||||||
|
|
||||||
|
// 处理 input 字段,如果它是字符串,则转换为消息数组
|
||||||
|
if (codexRequest.input && typeof codexRequest.input === 'string') {
|
||||||
|
const inputText = codexRequest.input;
|
||||||
|
codexRequest.input = [{
|
||||||
|
type: "message",
|
||||||
|
role: "user",
|
||||||
|
content: [{
|
||||||
|
type: "input_text",
|
||||||
|
text: inputText
|
||||||
|
}]
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置Codex特定的字段
|
||||||
|
codexRequest.stream = true;
|
||||||
|
codexRequest.store = false;
|
||||||
|
codexRequest.parallel_tool_calls = true;
|
||||||
|
codexRequest.include = ['reasoning.encrypted_content'];
|
||||||
|
|
||||||
|
// 删除Codex不支持的字段
|
||||||
|
delete codexRequest.max_output_tokens;
|
||||||
|
delete codexRequest.max_completion_tokens;
|
||||||
|
delete codexRequest.temperature;
|
||||||
|
delete codexRequest.top_p;
|
||||||
|
delete codexRequest.service_tier;
|
||||||
|
delete codexRequest.user;
|
||||||
|
delete codexRequest.reasoning;
|
||||||
|
|
||||||
|
// 添加 reasoning 配置
|
||||||
|
codexRequest.reasoning = {
|
||||||
|
"effort": "medium",
|
||||||
|
"summary": "auto"
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// 确保 input 数组中的每个项都有 type: "message",并将系统角色转换为开发者角色
|
||||||
|
if (codexRequest.input && Array.isArray(codexRequest.input)) {
|
||||||
|
codexRequest.input = codexRequest.input.map(item => {
|
||||||
|
// 如果没有 type 或者 type 不是 message,则添加 type: "message"
|
||||||
|
if (!item.type || item.type !== 'message') {
|
||||||
|
item = { type: "message", ...item };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将系统角色转换为开发者角色
|
||||||
|
if (item.role === 'system') {
|
||||||
|
item = { ...item, role: 'developer' };
|
||||||
|
}
|
||||||
|
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return codexRequest;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* OpenAI → Codex 请求转换
|
* OpenAI → Codex 请求转换
|
||||||
*/
|
*/
|
||||||
toCodexRequest(data) {
|
toOpenAIRequestToCodexRequest(data) {
|
||||||
// 构建工具名称映射
|
// 构建工具名称映射
|
||||||
this.buildToolNameMap(data.tools || []);
|
this.buildToolNameMap(data.tools || []);
|
||||||
|
|
||||||
|
|
@ -536,6 +593,210 @@ export class CodexConverter extends BaseConverter {
|
||||||
return openaiResponse;
|
return openaiResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Codex → OpenAI Responses 响应转换
|
||||||
|
*/
|
||||||
|
toOpenAIResponsesResponse(rawJSON, model) {
|
||||||
|
const root = typeof rawJSON === 'string' ? JSON.parse(rawJSON) : rawJSON;
|
||||||
|
if (root.type !== 'response.completed') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = root.response;
|
||||||
|
const unixTimestamp = response.created_at || Math.floor(Date.now() / 1000);
|
||||||
|
|
||||||
|
const output = [];
|
||||||
|
|
||||||
|
if (response.output && Array.isArray(response.output)) {
|
||||||
|
for (const item of response.output) {
|
||||||
|
if (item.type === 'reasoning') {
|
||||||
|
let reasoningText = '';
|
||||||
|
if (Array.isArray(item.summary)) {
|
||||||
|
const summaryItem = item.summary.find(s => s.type === 'summary_text');
|
||||||
|
if (summaryItem) reasoningText = summaryItem.text;
|
||||||
|
}
|
||||||
|
if (reasoningText) {
|
||||||
|
output.push({
|
||||||
|
id: `msg_${uuidv4().replace(/-/g, '')}`,
|
||||||
|
type: "message",
|
||||||
|
role: "assistant",
|
||||||
|
status: "completed",
|
||||||
|
content: [{
|
||||||
|
type: "reasoning",
|
||||||
|
text: reasoningText
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (item.type === 'message') {
|
||||||
|
let contentText = '';
|
||||||
|
if (Array.isArray(item.content)) {
|
||||||
|
const contentItem = item.content.find(c => c.type === 'output_text');
|
||||||
|
if (contentItem) contentText = contentItem.text;
|
||||||
|
}
|
||||||
|
if (contentText) {
|
||||||
|
output.push({
|
||||||
|
id: `msg_${uuidv4().replace(/-/g, '')}`,
|
||||||
|
type: "message",
|
||||||
|
role: "assistant",
|
||||||
|
status: "completed",
|
||||||
|
content: [{
|
||||||
|
type: "output_text",
|
||||||
|
text: contentText,
|
||||||
|
annotations: []
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (item.type === 'function_call') {
|
||||||
|
output.push({
|
||||||
|
id: item.call_id || `call_${uuidv4().replace(/-/g, '')}`,
|
||||||
|
type: "function_call",
|
||||||
|
name: this.getOriginalToolName(item.name),
|
||||||
|
arguments: typeof item.arguments === 'string' ? item.arguments : JSON.stringify(item.arguments),
|
||||||
|
status: "completed"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: response.id || `resp_${uuidv4().replace(/-/g, '')}`,
|
||||||
|
object: "response",
|
||||||
|
created_at: unixTimestamp,
|
||||||
|
model: response.model || model,
|
||||||
|
status: "completed",
|
||||||
|
output: output,
|
||||||
|
incomplete_details: response.incomplete_details || null,
|
||||||
|
usage: {
|
||||||
|
input_tokens: response.usage?.input_tokens || 0,
|
||||||
|
output_tokens: response.usage?.output_tokens || 0,
|
||||||
|
total_tokens: response.usage?.total_tokens || 0,
|
||||||
|
output_tokens_details: {
|
||||||
|
reasoning_tokens: response.usage?.output_tokens_details?.reasoning_tokens || 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Codex → Gemini 响应转换
|
||||||
|
*/
|
||||||
|
toGeminiResponse(rawJSON, model) {
|
||||||
|
const root = typeof rawJSON === 'string' ? JSON.parse(rawJSON) : rawJSON;
|
||||||
|
if (root.type !== 'response.completed') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = root.response;
|
||||||
|
const parts = [];
|
||||||
|
|
||||||
|
if (response.output && Array.isArray(response.output)) {
|
||||||
|
for (const item of response.output) {
|
||||||
|
if (item.type === 'reasoning') {
|
||||||
|
let reasoningText = '';
|
||||||
|
if (Array.isArray(item.summary)) {
|
||||||
|
const summaryItem = item.summary.find(s => s.type === 'summary_text');
|
||||||
|
if (summaryItem) reasoningText = summaryItem.text;
|
||||||
|
}
|
||||||
|
if (reasoningText) {
|
||||||
|
parts.push({ text: reasoningText, thought: true });
|
||||||
|
}
|
||||||
|
} else if (item.type === 'message') {
|
||||||
|
let contentText = '';
|
||||||
|
if (Array.isArray(item.content)) {
|
||||||
|
const contentItem = item.content.find(c => c.type === 'output_text');
|
||||||
|
if (contentItem) contentText = contentItem.text;
|
||||||
|
}
|
||||||
|
if (contentText) {
|
||||||
|
parts.push({ text: contentText });
|
||||||
|
}
|
||||||
|
} else if (item.type === 'function_call') {
|
||||||
|
parts.push({
|
||||||
|
functionCall: {
|
||||||
|
name: this.getOriginalToolName(item.name),
|
||||||
|
args: typeof item.arguments === 'string' ? JSON.parse(item.arguments) : item.arguments
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
candidates: [{
|
||||||
|
content: {
|
||||||
|
role: "model",
|
||||||
|
parts: parts
|
||||||
|
},
|
||||||
|
finishReason: "STOP"
|
||||||
|
}],
|
||||||
|
usageMetadata: {
|
||||||
|
promptTokenCount: response.usage?.input_tokens || 0,
|
||||||
|
candidatesTokenCount: response.usage?.output_tokens || 0,
|
||||||
|
totalTokenCount: response.usage?.total_tokens || 0
|
||||||
|
},
|
||||||
|
modelVersion: response.model || model,
|
||||||
|
responseId: response.id
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Codex → Claude 响应转换
|
||||||
|
*/
|
||||||
|
toClaudeResponse(rawJSON, model) {
|
||||||
|
const root = typeof rawJSON === 'string' ? JSON.parse(rawJSON) : rawJSON;
|
||||||
|
if (root.type !== 'response.completed') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = root.response;
|
||||||
|
const content = [];
|
||||||
|
let stopReason = "end_turn";
|
||||||
|
|
||||||
|
if (response.output && Array.isArray(response.output)) {
|
||||||
|
for (const item of response.output) {
|
||||||
|
if (item.type === 'reasoning') {
|
||||||
|
let reasoningText = '';
|
||||||
|
if (Array.isArray(item.summary)) {
|
||||||
|
const summaryItem = item.summary.find(s => s.type === 'summary_text');
|
||||||
|
if (summaryItem) reasoningText = summaryItem.text;
|
||||||
|
}
|
||||||
|
if (reasoningText) {
|
||||||
|
content.push({ type: "thinking", thinking: reasoningText });
|
||||||
|
}
|
||||||
|
} else if (item.type === 'message') {
|
||||||
|
let contentText = '';
|
||||||
|
if (Array.isArray(item.content)) {
|
||||||
|
const contentItem = item.content.find(c => c.type === 'output_text');
|
||||||
|
if (contentItem) contentText = contentItem.text;
|
||||||
|
}
|
||||||
|
if (contentText) {
|
||||||
|
content.push({ type: "text", text: contentText });
|
||||||
|
}
|
||||||
|
} else if (item.type === 'function_call') {
|
||||||
|
stopReason = "tool_use";
|
||||||
|
content.push({
|
||||||
|
type: "tool_use",
|
||||||
|
id: item.call_id || `call_${uuidv4().replace(/-/g, '')}`,
|
||||||
|
name: this.getOriginalToolName(item.name),
|
||||||
|
input: typeof item.arguments === 'string' ? JSON.parse(item.arguments) : item.arguments
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: response.id || `msg_${uuidv4().replace(/-/g, '')}`,
|
||||||
|
type: "message",
|
||||||
|
role: "assistant",
|
||||||
|
model: response.model || model,
|
||||||
|
content: content,
|
||||||
|
stop_reason: stopReason,
|
||||||
|
usage: {
|
||||||
|
input_tokens: response.usage?.input_tokens || 0,
|
||||||
|
output_tokens: response.usage?.output_tokens || 0
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Codex → OpenAI 流式响应块转换
|
* Codex → OpenAI 流式响应块转换
|
||||||
*/
|
*/
|
||||||
|
|
@ -699,222 +960,14 @@ export class CodexConverter extends BaseConverter {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 转换模型列表
|
|
||||||
*/
|
|
||||||
convertModelList(data, targetProtocol) {
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Codex → OpenAI Responses 响应转换
|
|
||||||
*/
|
|
||||||
toOpenAIResponsesResponse(rawJSON, model) {
|
|
||||||
const root = typeof rawJSON === 'string' ? JSON.parse(rawJSON) : rawJSON;
|
|
||||||
if (root.type !== 'response.completed') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = root.response;
|
|
||||||
const unixTimestamp = response.created_at || Math.floor(Date.now() / 1000);
|
|
||||||
|
|
||||||
const output = [];
|
|
||||||
let hasToolCall = false;
|
|
||||||
|
|
||||||
if (response.output && Array.isArray(response.output)) {
|
|
||||||
for (const item of response.output) {
|
|
||||||
if (item.type === 'reasoning') {
|
|
||||||
let reasoningText = '';
|
|
||||||
if (Array.isArray(item.summary)) {
|
|
||||||
const summaryItem = item.summary.find(s => s.type === 'summary_text');
|
|
||||||
if (summaryItem) reasoningText = summaryItem.text;
|
|
||||||
}
|
|
||||||
if (reasoningText) {
|
|
||||||
output.push({
|
|
||||||
id: `msg_${uuidv4().replace(/-/g, '')}`,
|
|
||||||
type: "message",
|
|
||||||
role: "assistant",
|
|
||||||
status: "completed",
|
|
||||||
content: [{
|
|
||||||
type: "reasoning",
|
|
||||||
text: reasoningText
|
|
||||||
}]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else if (item.type === 'message') {
|
|
||||||
let contentText = '';
|
|
||||||
if (Array.isArray(item.content)) {
|
|
||||||
const contentItem = item.content.find(c => c.type === 'output_text');
|
|
||||||
if (contentItem) contentText = contentItem.text;
|
|
||||||
}
|
|
||||||
if (contentText) {
|
|
||||||
output.push({
|
|
||||||
id: `msg_${uuidv4().replace(/-/g, '')}`,
|
|
||||||
type: "message",
|
|
||||||
role: "assistant",
|
|
||||||
status: "completed",
|
|
||||||
content: [{
|
|
||||||
type: "output_text",
|
|
||||||
text: contentText,
|
|
||||||
annotations: []
|
|
||||||
}]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else if (item.type === 'function_call') {
|
|
||||||
hasToolCall = true;
|
|
||||||
output.push({
|
|
||||||
id: item.call_id || `call_${uuidv4().replace(/-/g, '')}`,
|
|
||||||
type: "function_call",
|
|
||||||
name: this.getOriginalToolName(item.name),
|
|
||||||
arguments: typeof item.arguments === 'string' ? item.arguments : JSON.stringify(item.arguments),
|
|
||||||
status: "completed"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: response.id || `resp_${uuidv4().replace(/-/g, '')}`,
|
|
||||||
object: "response",
|
|
||||||
created_at: unixTimestamp,
|
|
||||||
model: response.model || model,
|
|
||||||
status: "completed",
|
|
||||||
output: output,
|
|
||||||
usage: {
|
|
||||||
input_tokens: response.usage?.input_tokens || 0,
|
|
||||||
output_tokens: response.usage?.output_tokens || 0,
|
|
||||||
total_tokens: response.usage?.total_tokens || 0,
|
|
||||||
output_tokens_details: {
|
|
||||||
reasoning_tokens: response.usage?.output_tokens_details?.reasoning_tokens || 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Codex → Gemini 响应转换
|
|
||||||
*/
|
|
||||||
toGeminiResponse(rawJSON, model) {
|
|
||||||
const root = typeof rawJSON === 'string' ? JSON.parse(rawJSON) : rawJSON;
|
|
||||||
if (root.type !== 'response.completed') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = root.response;
|
|
||||||
const parts = [];
|
|
||||||
|
|
||||||
if (response.output && Array.isArray(response.output)) {
|
|
||||||
for (const item of response.output) {
|
|
||||||
if (item.type === 'reasoning') {
|
|
||||||
let reasoningText = '';
|
|
||||||
if (Array.isArray(item.summary)) {
|
|
||||||
const summaryItem = item.summary.find(s => s.type === 'summary_text');
|
|
||||||
if (summaryItem) reasoningText = summaryItem.text;
|
|
||||||
}
|
|
||||||
if (reasoningText) {
|
|
||||||
parts.push({ text: reasoningText, thought: true });
|
|
||||||
}
|
|
||||||
} else if (item.type === 'message') {
|
|
||||||
let contentText = '';
|
|
||||||
if (Array.isArray(item.content)) {
|
|
||||||
const contentItem = item.content.find(c => c.type === 'output_text');
|
|
||||||
if (contentItem) contentText = contentItem.text;
|
|
||||||
}
|
|
||||||
if (contentText) {
|
|
||||||
parts.push({ text: contentText });
|
|
||||||
}
|
|
||||||
} else if (item.type === 'function_call') {
|
|
||||||
parts.push({
|
|
||||||
functionCall: {
|
|
||||||
name: this.getOriginalToolName(item.name),
|
|
||||||
args: typeof item.arguments === 'string' ? JSON.parse(item.arguments) : item.arguments
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
candidates: [{
|
|
||||||
content: {
|
|
||||||
role: "model",
|
|
||||||
parts: parts
|
|
||||||
},
|
|
||||||
finishReason: "STOP"
|
|
||||||
}],
|
|
||||||
usageMetadata: {
|
|
||||||
promptTokenCount: response.usage?.input_tokens || 0,
|
|
||||||
candidatesTokenCount: response.usage?.output_tokens || 0,
|
|
||||||
totalTokenCount: response.usage?.total_tokens || 0
|
|
||||||
},
|
|
||||||
modelVersion: response.model || model,
|
|
||||||
responseId: response.id
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Codex → Claude 响应转换
|
|
||||||
*/
|
|
||||||
toClaudeResponse(rawJSON, model) {
|
|
||||||
const root = typeof rawJSON === 'string' ? JSON.parse(rawJSON) : rawJSON;
|
|
||||||
if (root.type !== 'response.completed') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = root.response;
|
|
||||||
const content = [];
|
|
||||||
let stopReason = "end_turn";
|
|
||||||
|
|
||||||
if (response.output && Array.isArray(response.output)) {
|
|
||||||
for (const item of response.output) {
|
|
||||||
if (item.type === 'reasoning') {
|
|
||||||
let reasoningText = '';
|
|
||||||
if (Array.isArray(item.summary)) {
|
|
||||||
const summaryItem = item.summary.find(s => s.type === 'summary_text');
|
|
||||||
if (summaryItem) reasoningText = summaryItem.text;
|
|
||||||
}
|
|
||||||
if (reasoningText) {
|
|
||||||
content.push({ type: "thinking", thinking: reasoningText });
|
|
||||||
}
|
|
||||||
} else if (item.type === 'message') {
|
|
||||||
let contentText = '';
|
|
||||||
if (Array.isArray(item.content)) {
|
|
||||||
const contentItem = item.content.find(c => c.type === 'output_text');
|
|
||||||
if (contentItem) contentText = contentItem.text;
|
|
||||||
}
|
|
||||||
if (contentText) {
|
|
||||||
content.push({ type: "text", text: contentText });
|
|
||||||
}
|
|
||||||
} else if (item.type === 'function_call') {
|
|
||||||
stopReason = "tool_use";
|
|
||||||
content.push({
|
|
||||||
type: "tool_use",
|
|
||||||
id: item.call_id || `call_${uuidv4().replace(/-/g, '')}`,
|
|
||||||
name: this.getOriginalToolName(item.name),
|
|
||||||
input: typeof item.arguments === 'string' ? JSON.parse(item.arguments) : item.arguments
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: response.id || `msg_${uuidv4().replace(/-/g, '')}`,
|
|
||||||
type: "message",
|
|
||||||
role: "assistant",
|
|
||||||
model: response.model || model,
|
|
||||||
content: content,
|
|
||||||
stop_reason: stopReason,
|
|
||||||
usage: {
|
|
||||||
input_tokens: response.usage?.input_tokens || 0,
|
|
||||||
output_tokens: response.usage?.output_tokens || 0
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Codex → OpenAI Responses 流式响应转换
|
* Codex → OpenAI Responses 流式响应转换
|
||||||
*/
|
*/
|
||||||
toOpenAIResponsesStreamChunk(chunk, model) {
|
toOpenAIResponsesStreamChunk(chunk, model) {
|
||||||
|
if(true){
|
||||||
|
return chunk;
|
||||||
|
}
|
||||||
|
|
||||||
const type = chunk.type;
|
const type = chunk.type;
|
||||||
const resId = chunk.response?.id || 'default';
|
const resId = chunk.response?.id || 'default';
|
||||||
|
|
||||||
|
|
@ -1162,10 +1215,4 @@ export class CodexConverter extends BaseConverter {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 转换模型列表
|
|
||||||
*/
|
|
||||||
convertModelList(data, targetProtocol) {
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1340,7 +1340,7 @@ export class OpenAIConverter extends BaseConverter {
|
||||||
* OpenAI请求 -> Codex请求(委托给 CodexConverter)
|
* OpenAI请求 -> Codex请求(委托给 CodexConverter)
|
||||||
*/
|
*/
|
||||||
toCodexRequest(openaiRequest) {
|
toCodexRequest(openaiRequest) {
|
||||||
return this.codexConverter.toCodexRequest(openaiRequest);
|
return this.codexConverter.toOpenAIRequestToCodexRequest(openaiRequest);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
|
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import { BaseConverter } from '../BaseConverter.js';
|
import { BaseConverter } from '../BaseConverter.js';
|
||||||
|
import { CodexConverter } from './CodexConverter.js';
|
||||||
import { MODEL_PROTOCOL_PREFIX } from '../../utils/common.js';
|
import { MODEL_PROTOCOL_PREFIX } from '../../utils/common.js';
|
||||||
import {
|
import {
|
||||||
extractAndProcessSystemMessages as extractSystemMessages,
|
extractAndProcessSystemMessages as extractSystemMessages,
|
||||||
|
|
@ -31,6 +32,7 @@ import {
|
||||||
export class OpenAIResponsesConverter extends BaseConverter {
|
export class OpenAIResponsesConverter extends BaseConverter {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES);
|
super(MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES);
|
||||||
|
this.codexConverter = new CodexConverter();
|
||||||
}
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
@ -165,9 +167,9 @@ export class OpenAIResponsesConverter extends BaseConverter {
|
||||||
content = item.content;
|
content = item.content;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (content || item.role === 'assistant') {
|
if (content || (item.role === 'assistant' || item.role === 'developer')) {
|
||||||
openaiRequest.messages.push({
|
openaiRequest.messages.push({
|
||||||
role: item.role,
|
role: item.role === 'developer' ? 'assistant' : item.role,
|
||||||
content: content
|
content: content
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -210,19 +212,31 @@ export class OpenAIResponsesConverter extends BaseConverter {
|
||||||
|
|
||||||
// 处理工具
|
// 处理工具
|
||||||
if (responsesRequest.tools && Array.isArray(responsesRequest.tools)) {
|
if (responsesRequest.tools && Array.isArray(responsesRequest.tools)) {
|
||||||
openaiRequest.tools = responsesRequest.tools.map(tool => {
|
openaiRequest.tools = responsesRequest.tools
|
||||||
if (tool.type && tool.type !== 'function') {
|
.map(tool => {
|
||||||
return tool;
|
if (tool.type && tool.type !== 'function') {
|
||||||
}
|
return null;
|
||||||
return {
|
|
||||||
type: 'function',
|
|
||||||
function: {
|
|
||||||
name: tool.name,
|
|
||||||
description: tool.description,
|
|
||||||
parameters: tool.parameters || tool.parametersJsonSchema || { type: 'object', properties: {} }
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
});
|
const name = tool.name || (tool.function && tool.function.name);
|
||||||
|
const description = tool.description || (tool.function && tool.function.description);
|
||||||
|
const parameters = tool.parameters || (tool.function && tool.function.parameters) || tool.parametersJsonSchema || { type: 'object', properties: {} };
|
||||||
|
|
||||||
|
// 如果没有名称,则该工具无效,稍后过滤掉
|
||||||
|
if (!name) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'function',
|
||||||
|
function: {
|
||||||
|
name: name,
|
||||||
|
description: description,
|
||||||
|
parameters: parameters
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter(tool => tool !== null);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (responsesRequest.tool_choice) {
|
if (responsesRequest.tool_choice) {
|
||||||
|
|
@ -687,11 +701,13 @@ export class OpenAIResponsesConverter extends BaseConverter {
|
||||||
// 处理工具
|
// 处理工具
|
||||||
if (responsesRequest.tools && Array.isArray(responsesRequest.tools)) {
|
if (responsesRequest.tools && Array.isArray(responsesRequest.tools)) {
|
||||||
geminiRequest.tools = [{
|
geminiRequest.tools = [{
|
||||||
functionDeclarations: responsesRequest.tools.map(tool => ({
|
functionDeclarations: responsesRequest.tools
|
||||||
name: tool.name,
|
.filter(tool => !tool.type || tool.type === 'function')
|
||||||
description: tool.description,
|
.map(tool => ({
|
||||||
parameters: tool.parameters || tool.parametersJsonSchema || { type: 'object', properties: {} }
|
name: tool.name,
|
||||||
}))
|
description: tool.description,
|
||||||
|
parameters: tool.parameters || tool.parametersJsonSchema || { type: 'object', properties: {} }
|
||||||
|
}))
|
||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -780,6 +796,13 @@ export class OpenAIResponsesConverter extends BaseConverter {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OpenAI Responses → Codex 请求转换
|
||||||
|
*/
|
||||||
|
toCodexRequest(responsesRequest) {
|
||||||
|
return this.codexConverter.toOpenAIResponsesToCodexRequest(responsesRequest);
|
||||||
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// 辅助方法
|
// 辅助方法
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
@ -850,83 +873,6 @@ export class OpenAIResponsesConverter extends BaseConverter {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// 转换到 Codex 格式
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* OpenAI Responses → Codex 请求转换
|
|
||||||
*/
|
|
||||||
toCodexRequest(responsesRequest) {
|
|
||||||
const codexRequest = {
|
|
||||||
model: responsesRequest.model,
|
|
||||||
instructions: responsesRequest.instructions || '',
|
|
||||||
input: [],
|
|
||||||
stream: responsesRequest.stream || false,
|
|
||||||
store: false,
|
|
||||||
reasoning: {
|
|
||||||
effort: responsesRequest.reasoning?.effort || 'medium',
|
|
||||||
summary: 'auto'
|
|
||||||
},
|
|
||||||
parallel_tool_calls: responsesRequest.parallel_tool_calls ?? true,
|
|
||||||
include: ['reasoning.encrypted_content']
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理 input
|
|
||||||
if (responsesRequest.input && Array.isArray(responsesRequest.input)) {
|
|
||||||
for (const item of responsesRequest.input) {
|
|
||||||
const itemType = item.type || (item.role ? 'message' : '');
|
|
||||||
|
|
||||||
if (itemType === 'message') {
|
|
||||||
const content = [];
|
|
||||||
if (Array.isArray(item.content)) {
|
|
||||||
item.content.forEach(c => {
|
|
||||||
content.push({
|
|
||||||
type: item.role === 'assistant' ? 'output_text' : 'input_text',
|
|
||||||
text: c.text
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else if (typeof item.content === 'string') {
|
|
||||||
content.push({
|
|
||||||
type: item.role === 'assistant' ? 'output_text' : 'input_text',
|
|
||||||
text: item.content
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
codexRequest.input.push({
|
|
||||||
type: 'message',
|
|
||||||
role: item.role === 'system' ? 'developer' : item.role,
|
|
||||||
content: content
|
|
||||||
});
|
|
||||||
} else if (itemType === 'function_call') {
|
|
||||||
codexRequest.input.push({
|
|
||||||
type: 'function_call',
|
|
||||||
call_id: item.call_id,
|
|
||||||
name: item.name,
|
|
||||||
arguments: typeof item.arguments === 'string' ? item.arguments : JSON.stringify(item.arguments)
|
|
||||||
});
|
|
||||||
} else if (itemType === 'function_call_output') {
|
|
||||||
codexRequest.input.push({
|
|
||||||
type: 'function_call_output',
|
|
||||||
call_id: item.call_id,
|
|
||||||
output: item.output
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理工具
|
|
||||||
if (responsesRequest.tools) {
|
|
||||||
codexRequest.tools = responsesRequest.tools.map(tool => ({
|
|
||||||
type: 'function',
|
|
||||||
name: tool.name,
|
|
||||||
description: tool.description,
|
|
||||||
parameters: tool.parameters || tool.parametersJsonSchema || { type: 'object', properties: {} }
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
return codexRequest;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* OpenAI Responses → Codex 响应转换 (实际上是 Codex 转 OpenAI Responses)
|
* OpenAI Responses → Codex 响应转换 (实际上是 Codex 转 OpenAI Responses)
|
||||||
|
|
|
||||||
|
|
@ -74,8 +74,8 @@ class CodexResponsesAPIStrategy extends ProviderStrategy {
|
||||||
if (typeof requestBody.input === 'string') {
|
if (typeof requestBody.input === 'string') {
|
||||||
// Convert to array format to add system message
|
// Convert to array format to add system message
|
||||||
requestBody.input = [
|
requestBody.input = [
|
||||||
{ role: 'developer', content: filePromptContent },
|
{ type: 'message', role: 'developer', content: filePromptContent },
|
||||||
{ role: 'user', content: requestBody.input }
|
{ type: 'message', role: 'user', content: requestBody.input }
|
||||||
];
|
];
|
||||||
} else if (Array.isArray(requestBody.input)) {
|
} else if (Array.isArray(requestBody.input)) {
|
||||||
// Check if system message already exists
|
// Check if system message already exists
|
||||||
|
|
@ -86,11 +86,11 @@ class CodexResponsesAPIStrategy extends ProviderStrategy {
|
||||||
if (systemMessageIndex !== -1) {
|
if (systemMessageIndex !== -1) {
|
||||||
requestBody.input[systemMessageIndex].content = filePromptContent;
|
requestBody.input[systemMessageIndex].content = filePromptContent;
|
||||||
} else {
|
} else {
|
||||||
requestBody.input.unshift({ role: 'developer', content: filePromptContent });
|
requestBody.input.unshift({ type: 'message', role: 'developer', content: filePromptContent });
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// If input is not defined, initialize with system message
|
// If input is not defined, initialize with system message
|
||||||
requestBody.input = [{ role: 'developer', content: filePromptContent }];
|
requestBody.input = [{ type: 'message', role: 'developer', content: filePromptContent }];
|
||||||
}
|
}
|
||||||
} else if (requestBody.instructions) {
|
} else if (requestBody.instructions) {
|
||||||
// If system prompt mode is not append, then replace the instructions
|
// If system prompt mode is not append, then replace the instructions
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ const DEFAULT_LOCK_CONFIG = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const DEFAULT_QWEN_OAUTH_BASE_URL = 'https://chat.qwen.ai';
|
const DEFAULT_QWEN_OAUTH_BASE_URL = 'https://chat.qwen.ai';
|
||||||
const DEFAULT_QWEN_BASE_URL = 'https://portal.qwen.ai/v1';
|
const DEFAULT_QWEN_BASE_URL = 'https://dashscope.aliyuncs.com/compatible-mode/v1';
|
||||||
const QWEN_OAUTH_CLIENT_ID = 'f0304373b74a44d2b584a3fb70ca9e56';
|
const QWEN_OAUTH_CLIENT_ID = 'f0304373b74a44d2b584a3fb70ca9e56';
|
||||||
const QWEN_OAUTH_SCOPE = 'openid profile email model.completion';
|
const QWEN_OAUTH_SCOPE = 'openid profile email model.completion';
|
||||||
const QWEN_OAUTH_GRANT_TYPE = 'urn:ietf:params:oauth:grant-type:device_code';
|
const QWEN_OAUTH_GRANT_TYPE = 'urn:ietf:params:oauth:grant-type:device_code';
|
||||||
|
|
@ -531,7 +531,7 @@ export class QwenApiService {
|
||||||
const maxRetries = (this.config && this.config.REQUEST_MAX_RETRIES) || 3;
|
const maxRetries = (this.config && this.config.REQUEST_MAX_RETRIES) || 3;
|
||||||
const baseDelay = (this.config && this.config.REQUEST_BASE_DELAY) || 1000;
|
const baseDelay = (this.config && this.config.REQUEST_BASE_DELAY) || 1000;
|
||||||
|
|
||||||
const version = "0.2.1";
|
const version = "0.10.1";
|
||||||
const userAgent = `QwenCode/${version} (${process.platform}; ${process.arch})`;
|
const userAgent = `QwenCode/${version} (${process.platform}; ${process.arch})`;
|
||||||
logger.info(`[QwenApiService] User-Agent: ${userAgent}`);
|
logger.info(`[QwenApiService] User-Agent: ${userAgent}`);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,9 @@ export const PROVIDER_MODELS = {
|
||||||
'openaiResponses-custom': [],
|
'openaiResponses-custom': [],
|
||||||
'openai-qwen-oauth': [
|
'openai-qwen-oauth': [
|
||||||
'qwen3-coder-plus',
|
'qwen3-coder-plus',
|
||||||
'qwen3-coder-flash'
|
'qwen3-coder-flash',
|
||||||
|
'coder-model',
|
||||||
|
'vision-model'
|
||||||
],
|
],
|
||||||
'openai-iflow': [
|
'openai-iflow': [
|
||||||
// iFlow 特有模型
|
// iFlow 特有模型
|
||||||
|
|
|
||||||
|
|
@ -62,30 +62,14 @@ export async function handleAPIRequests(method, path, req, res, currentConfig, a
|
||||||
* @param {Object} services - The initialized services
|
* @param {Object} services - The initialized services
|
||||||
* @returns {Function} - The heartbeat and token refresh function
|
* @returns {Function} - The heartbeat and token refresh function
|
||||||
*/
|
*/
|
||||||
export function initializeAPIManagement(services, config = {}) {
|
export function initializeAPIManagement(services) {
|
||||||
const providerPoolManager = getProviderPoolManager();
|
const providerPoolManager = getProviderPoolManager();
|
||||||
const healthCheckInterval = config.HEALTH_CHECK_INTERVAL || 10 * 60 * 1000; // 默认10分钟
|
|
||||||
|
|
||||||
return async function heartbeatAndRefreshToken() {
|
return async function heartbeatAndRefreshToken() {
|
||||||
logger.info(`[Heartbeat] Server is running. Current time: ${new Date().toLocaleString()}`, Object.keys(services));
|
logger.info(`[Heartbeat] Server is running. Current time: ${new Date().toLocaleString()}`, Object.keys(services));
|
||||||
|
|
||||||
// 定期执行健康检查
|
|
||||||
if (providerPoolManager) {
|
|
||||||
try {
|
|
||||||
logger.info('[HealthCheck] Starting periodic health check...');
|
|
||||||
await providerPoolManager.performHealthChecks();
|
|
||||||
const stats = {};
|
|
||||||
for (const providerType in providerPoolManager.providerStatus) {
|
|
||||||
const providerStats = providerPoolManager.getProviderStats(providerType);
|
|
||||||
stats[providerType] = providerStats;
|
|
||||||
}
|
|
||||||
logger.info('[HealthCheck] Health check completed. Stats:', JSON.stringify(stats));
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('[HealthCheck] Health check failed:', error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 循环遍历所有已初始化的服务适配器,并尝试刷新令牌
|
// 循环遍历所有已初始化的服务适配器,并尝试刷新令牌
|
||||||
|
// if (getProviderPoolManager()) {
|
||||||
|
// await getProviderPoolManager().performHealthChecks(); // 定期执行健康检查
|
||||||
|
// }
|
||||||
for (const providerKey in services) {
|
for (const providerKey in services) {
|
||||||
const serviceAdapter = services[providerKey];
|
const serviceAdapter = services[providerKey];
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -265,7 +265,7 @@ async function startServer() {
|
||||||
initializeUIManagement(CONFIG);
|
initializeUIManagement(CONFIG);
|
||||||
|
|
||||||
// Initialize API management and get heartbeat function
|
// Initialize API management and get heartbeat function
|
||||||
const heartbeatAndRefreshToken = initializeAPIManagement(services, CONFIG);
|
const heartbeatAndRefreshToken = initializeAPIManagement(services);
|
||||||
|
|
||||||
// Create request handler
|
// Create request handler
|
||||||
const requestHandlerInstance = createRequestHandler(CONFIG, getProviderPoolManager());
|
const requestHandlerInstance = createRequestHandler(CONFIG, getProviderPoolManager());
|
||||||
|
|
|
||||||
|
|
@ -594,11 +594,8 @@ export async function handleStreamRequest(res, service, model, requestBody, from
|
||||||
hasMessageStop = true;
|
hasMessageStop = true;
|
||||||
}
|
}
|
||||||
} else if (clientProtocol === MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES) {
|
} else if (clientProtocol === MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES) {
|
||||||
if (!hasMessageStop) {
|
// OpenAI Responses 以 response.completed/response.incomplete(或 error)作为结束事件。
|
||||||
res.write('event: done\n');
|
// 连接关闭即表示流结束;不要再追加 `event: done` + `data: {}`,否则会触发下游类型校验失败(AI_TypeValidationError)。
|
||||||
res.write('data: {}\n\n');
|
|
||||||
hasMessageStop = true;
|
|
||||||
}
|
|
||||||
} else if (clientProtocol === MODEL_PROTOCOL_PREFIX.CLAUDE) {
|
} else if (clientProtocol === MODEL_PROTOCOL_PREFIX.CLAUDE) {
|
||||||
if (!hasMessageStop) {
|
if (!hasMessageStop) {
|
||||||
res.write('event: message_stop\n');
|
res.write('event: message_stop\n');
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue