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) {
|
||||
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}`);
|
||||
}
|
||||
|
||||
|
|
@ -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);
|
||||
case MODEL_PROTOCOL_PREFIX.CLAUDE:
|
||||
return this.toClaudeStreamChunk(chunk, model);
|
||||
case MODEL_PROTOCOL_PREFIX.CODEX:
|
||||
return chunk; // Codex to Codex
|
||||
default:
|
||||
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 请求转换
|
||||
*/
|
||||
toCodexRequest(data) {
|
||||
toOpenAIRequestToCodexRequest(data) {
|
||||
// 构建工具名称映射
|
||||
this.buildToolNameMap(data.tools || []);
|
||||
|
||||
|
|
@ -536,6 +593,210 @@ export class CodexConverter extends BaseConverter {
|
|||
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 流式响应块转换
|
||||
*/
|
||||
|
|
@ -699,222 +960,14 @@ export class CodexConverter extends BaseConverter {
|
|||
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 流式响应转换
|
||||
*/
|
||||
toOpenAIResponsesStreamChunk(chunk, model) {
|
||||
if(true){
|
||||
return chunk;
|
||||
}
|
||||
|
||||
const type = chunk.type;
|
||||
const resId = chunk.response?.id || 'default';
|
||||
|
||||
|
|
@ -1162,10 +1215,4 @@ export class CodexConverter extends BaseConverter {
|
|||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换模型列表
|
||||
*/
|
||||
convertModelList(data, targetProtocol) {
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1340,7 +1340,7 @@ export class OpenAIConverter extends BaseConverter {
|
|||
* OpenAI请求 -> Codex请求(委托给 CodexConverter)
|
||||
*/
|
||||
toCodexRequest(openaiRequest) {
|
||||
return this.codexConverter.toCodexRequest(openaiRequest);
|
||||
return this.codexConverter.toOpenAIRequestToCodexRequest(openaiRequest);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { BaseConverter } from '../BaseConverter.js';
|
||||
import { CodexConverter } from './CodexConverter.js';
|
||||
import { MODEL_PROTOCOL_PREFIX } from '../../utils/common.js';
|
||||
import {
|
||||
extractAndProcessSystemMessages as extractSystemMessages,
|
||||
|
|
@ -31,6 +32,7 @@ import {
|
|||
export class OpenAIResponsesConverter extends BaseConverter {
|
||||
constructor() {
|
||||
super(MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES);
|
||||
this.codexConverter = new CodexConverter();
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
|
|
@ -165,9 +167,9 @@ export class OpenAIResponsesConverter extends BaseConverter {
|
|||
content = item.content;
|
||||
}
|
||||
|
||||
if (content || item.role === 'assistant') {
|
||||
if (content || (item.role === 'assistant' || item.role === 'developer')) {
|
||||
openaiRequest.messages.push({
|
||||
role: item.role,
|
||||
role: item.role === 'developer' ? 'assistant' : item.role,
|
||||
content: content
|
||||
});
|
||||
}
|
||||
|
|
@ -210,19 +212,31 @@ export class OpenAIResponsesConverter extends BaseConverter {
|
|||
|
||||
// 处理工具
|
||||
if (responsesRequest.tools && Array.isArray(responsesRequest.tools)) {
|
||||
openaiRequest.tools = responsesRequest.tools.map(tool => {
|
||||
if (tool.type && tool.type !== 'function') {
|
||||
return tool;
|
||||
}
|
||||
return {
|
||||
type: 'function',
|
||||
function: {
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
parameters: tool.parameters || tool.parametersJsonSchema || { type: 'object', properties: {} }
|
||||
openaiRequest.tools = responsesRequest.tools
|
||||
.map(tool => {
|
||||
if (tool.type && tool.type !== 'function') {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
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) {
|
||||
|
|
@ -687,11 +701,13 @@ export class OpenAIResponsesConverter extends BaseConverter {
|
|||
// 处理工具
|
||||
if (responsesRequest.tools && Array.isArray(responsesRequest.tools)) {
|
||||
geminiRequest.tools = [{
|
||||
functionDeclarations: responsesRequest.tools.map(tool => ({
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
parameters: tool.parameters || tool.parametersJsonSchema || { type: 'object', properties: {} }
|
||||
}))
|
||||
functionDeclarations: responsesRequest.tools
|
||||
.filter(tool => !tool.type || tool.type === 'function')
|
||||
.map(tool => ({
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
|
|
|
|||
|
|
@ -74,8 +74,8 @@ class CodexResponsesAPIStrategy extends ProviderStrategy {
|
|||
if (typeof requestBody.input === 'string') {
|
||||
// Convert to array format to add system message
|
||||
requestBody.input = [
|
||||
{ role: 'developer', content: filePromptContent },
|
||||
{ role: 'user', content: requestBody.input }
|
||||
{ type: 'message', role: 'developer', content: filePromptContent },
|
||||
{ type: 'message', role: 'user', content: requestBody.input }
|
||||
];
|
||||
} else if (Array.isArray(requestBody.input)) {
|
||||
// Check if system message already exists
|
||||
|
|
@ -86,11 +86,11 @@ class CodexResponsesAPIStrategy extends ProviderStrategy {
|
|||
if (systemMessageIndex !== -1) {
|
||||
requestBody.input[systemMessageIndex].content = filePromptContent;
|
||||
} else {
|
||||
requestBody.input.unshift({ role: 'developer', content: filePromptContent });
|
||||
requestBody.input.unshift({ type: 'message', role: 'developer', content: filePromptContent });
|
||||
}
|
||||
} else {
|
||||
// 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) {
|
||||
// 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_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_SCOPE = 'openid profile email model.completion';
|
||||
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 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})`;
|
||||
logger.info(`[QwenApiService] User-Agent: ${userAgent}`);
|
||||
|
||||
|
|
|
|||
|
|
@ -39,7 +39,9 @@ export const PROVIDER_MODELS = {
|
|||
'openaiResponses-custom': [],
|
||||
'openai-qwen-oauth': [
|
||||
'qwen3-coder-plus',
|
||||
'qwen3-coder-flash'
|
||||
'qwen3-coder-flash',
|
||||
'coder-model',
|
||||
'vision-model'
|
||||
],
|
||||
'openai-iflow': [
|
||||
// iFlow 特有模型
|
||||
|
|
|
|||
|
|
@ -62,30 +62,14 @@ export async function handleAPIRequests(method, path, req, res, currentConfig, a
|
|||
* @param {Object} services - The initialized services
|
||||
* @returns {Function} - The heartbeat and token refresh function
|
||||
*/
|
||||
export function initializeAPIManagement(services, config = {}) {
|
||||
export function initializeAPIManagement(services) {
|
||||
const providerPoolManager = getProviderPoolManager();
|
||||
const healthCheckInterval = config.HEALTH_CHECK_INTERVAL || 10 * 60 * 1000; // 默认10分钟
|
||||
|
||||
return async function heartbeatAndRefreshToken() {
|
||||
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) {
|
||||
const serviceAdapter = services[providerKey];
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -265,7 +265,7 @@ async function startServer() {
|
|||
initializeUIManagement(CONFIG);
|
||||
|
||||
// Initialize API management and get heartbeat function
|
||||
const heartbeatAndRefreshToken = initializeAPIManagement(services, CONFIG);
|
||||
const heartbeatAndRefreshToken = initializeAPIManagement(services);
|
||||
|
||||
// Create request handler
|
||||
const requestHandlerInstance = createRequestHandler(CONFIG, getProviderPoolManager());
|
||||
|
|
|
|||
|
|
@ -594,11 +594,8 @@ export async function handleStreamRequest(res, service, model, requestBody, from
|
|||
hasMessageStop = true;
|
||||
}
|
||||
} else if (clientProtocol === MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES) {
|
||||
if (!hasMessageStop) {
|
||||
res.write('event: done\n');
|
||||
res.write('data: {}\n\n');
|
||||
hasMessageStop = true;
|
||||
}
|
||||
// OpenAI Responses 以 response.completed/response.incomplete(或 error)作为结束事件。
|
||||
// 连接关闭即表示流结束;不要再追加 `event: done` + `data: {}`,否则会触发下游类型校验失败(AI_TypeValidationError)。
|
||||
} else if (clientProtocol === MODEL_PROTOCOL_PREFIX.CLAUDE) {
|
||||
if (!hasMessageStop) {
|
||||
res.write('event: message_stop\n');
|
||||
|
|
|
|||
Loading…
Reference in a new issue