feat(api): 添加 OpenAI Responses API 支持

新增对 OpenAI Responses API 端点的部分支持,包括请求转换、流式响应处理和供应商适配。主要变更:

- 新增 OpenAIResponsesApiService 核心服务实现
- 实现 Claude/Gemini 到 Responses API 的双向协议转换(能聊天,不能调用工具)
- 添加流式响应状态管理和事件生成机制
- 扩展路由支持 /v1/responses 端点
- 更新文档说明配置方法和使用示例
This commit is contained in:
hex2077 2025-10-16 23:26:36 +08:00
parent d36a9466a8
commit dfe7ce914e
11 changed files with 1332 additions and 55 deletions

View file

@ -123,6 +123,9 @@
* **使用前提**:使用 Kiro API 需要[下载 Kiro 客户端](https://aibook.ren/archives/kiro-install)并完成授权登录,以生成 `kiro-auth-token.json` 文件。
* **最佳体验**:推荐配合 Claude Code 使用以获得最佳体验。
* **注意事项**Kiro 服务政策已调整,请查阅官方公告了解具体使用限制。
* **OpenAI Responses API**:
* **功能说明**: 支持 OpenAI Responses API 端点,提供更结构化的对话响应能力,适用于需要高级对话管理的应用场景。
* **配置方法**: 在 `config.json` 或启动参数中设置 `MODEL_PROVIDER``openaiResponses-custom`,并提供相应的 API 密钥和基础 URL。
* **模型供应商切换**:本项目支持通过 Path 路由和环境变量两种方式,在 API 调用中灵活切换不同的模型供应商。
#### 通过 Path 路由切换
@ -132,6 +135,7 @@
* `http://localhost:3000/openai-custom` - 使用 OpenAI 自定义供应商处理 Claude 请求。
* `http://localhost:3000/gemini-cli-oauth` - 使用 Gemini CLI OAuth 供应商处理 Claude 请求。
* `http://localhost:3000/openai-qwen-oauth` - 使用 Qwen OAuth 供应商处理 Claude 请求。
* `http://localhost:3000/openaiResponses-custom` - 使用 OpenAI Responses API 供应商处理结构化对话请求。
这些 Path 路由不仅适用于直接 API 调用,也可在 Cline、Kilo 等编程 Agent 中配置 API 端点时使用,实现灵活的模型调用。例如,将 Agent 的 API 端点设置为 `http://localhost:3000/claude-kiro-oauth` 即可调用通过 Kiro OAuth 认证的 Claude 模型。
@ -293,6 +297,14 @@ $env:HTTP_PROXY="http://your_proxy_address:port"
|------|------|--------|------|
| `--qwen-oauth-creds-file` | string | null | Qwen OAuth 凭据 JSON 文件路径 (当 `model-provider``openai-qwen-oauth` 时必需) |
### 🔄 OpenAI Responses API 参数
| 参数 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `--model-provider` | string | openaiResponses-custom | 模型提供商使用OpenAI Responses API时设置为 `openaiResponses-custom` |
| `--openai-api-key` | string | null | OpenAI API 密钥 (当 `model-provider``openaiResponses-custom` 时必需) |
| `--openai-base-url` | string | null | OpenAI API 基础 URL (当 `model-provider``openaiResponses-custom` 时必需) |
### 📝 系统提示配置参数
| 参数 | 类型 | 默认值 | 说明 |
@ -342,6 +354,9 @@ node src/api-server.js --model-provider openai-custom --openai-api-key sk-xxx --
# 使用Claude提供商
node src/api-server.js --model-provider claude-custom --claude-api-key sk-ant-xxx --claude-base-url https://api.anthropic.com
# 使用OpenAI Responses API提供商
node src/api-server.js --model-provider openaiResponses-custom --openai-api-key sk-xxx --openai-base-url https://api.openai.com/v1
# 使用Gemini提供商Base64凭据
node src/api-server.js --model-provider gemini-cli-oauth --gemini-oauth-creds-base64 eyJ0eXBlIjoi... --project-id your-project-id

View file

@ -23,6 +23,19 @@
"lastErrorTime": null
}
],
"openaiResponses-custom": [
{
"OPENAI_API_KEY": "sk-openai-key",
"OPENAI_BASE_URL": "https://api.openai.com/v1",
"checkModelName": null,
"uuid": "e284628d-302f-456d-91f3-609538678968",
"isHealthy": true,
"lastUsed": null,
"usageCount": 0,
"errorCount": 0,
"lastErrorTime": null
}
],
"gemini-cli-oauth": [
{
"GEMINI_OAUTH_CREDS_FILE_PATH": "./credentials1.json",

View file

@ -1,3 +1,4 @@
import { OpenAIResponsesApiService } from './openai/openai-responses-core.js'; // 导入OpenAIResponsesApiService
import { GeminiApiService } from './gemini/gemini-core.js'; // 导入geminiApiService
import { OpenAIApiService } from './openai/openai-core.js'; // 导入OpenAIApiService
import { ClaudeApiService } from './claude/claude-core.js'; // 导入ClaudeApiService
@ -126,6 +127,35 @@ export class OpenAIApiServiceAdapter extends ApiServiceAdapter {
}
}
// OpenAI Responses API 服务适配器
export class OpenAIResponsesApiServiceAdapter extends ApiServiceAdapter {
constructor(config) {
super();
this.openAIResponsesApiService = new OpenAIResponsesApiService(config);
}
async generateContent(model, requestBody) {
// The adapter expects the requestBody to be in the OpenAI Responses format.
return this.openAIResponsesApiService.generateContent(model, requestBody);
}
async *generateContentStream(model, requestBody) {
// The adapter expects the requestBody to be in the OpenAI Responses format.
const stream = this.openAIResponsesApiService.generateContentStream(model, requestBody);
yield* stream;
}
async listModels() {
// The adapter returns the native model list from the underlying service.
return this.openAIResponsesApiService.listModels();
}
async refreshToken() {
// OpenAI API keys are typically static and do not require refreshing.
return Promise.resolve();
}
}
// Claude API 服务适配器
export class ClaudeApiServiceAdapter extends ApiServiceAdapter {
constructor(config) {
@ -254,6 +284,9 @@ export function getServiceAdapter(config) {
case MODEL_PROVIDER.OPENAI_CUSTOM:
serviceInstances[providerKey] = new OpenAIApiServiceAdapter(config);
break;
case MODEL_PROVIDER.OPENAI_CUSTOM_RESPONSES:
serviceInstances[providerKey] = new OpenAIResponsesApiServiceAdapter(config);
break;
case MODEL_PROVIDER.GEMINI_CLI:
serviceInstances[providerKey] = new GeminiApiServiceAdapter(config);
break;
@ -271,4 +304,4 @@ export function getServiceAdapter(config) {
}
}
return serviceInstances[providerKey];
}
}

View file

@ -649,6 +649,9 @@ function createRequestHandler(config) {
if (path === '/v1/chat/completions') {
return await handleContentGenerationRequest(req, res, apiService, ENDPOINT_TYPE.OPENAI_CHAT, currentConfig, PROMPT_LOG_FILENAME, providerPoolManager, currentConfig.uuid);
}
if (path === '/v1/responses') {
return await handleContentGenerationRequest(req, res, apiService, ENDPOINT_TYPE.OPENAI_RESPONSES, currentConfig, PROMPT_LOG_FILENAME, providerPoolManager, currentConfig.uuid);
}
const geminiUrlPattern = new RegExp(`/v1beta/models/(.+?):(${API_ACTIONS.GENERATE_CONTENT}|${API_ACTIONS.STREAM_GENERATE_CONTENT})`);
if (geminiUrlPattern.test(path)) {
return await handleContentGenerationRequest(req, res, apiService, ENDPOINT_TYPE.GEMINI_CONTENT, currentConfig, PROMPT_LOG_FILENAME, providerPoolManager, currentConfig.uuid);
@ -719,7 +722,7 @@ async function startServer() {
console.log(`------------------------------------------`);
console.log(`\nUnified API Server running on http://${CONFIG.HOST}:${CONFIG.SERVER_PORT}`);
console.log(`Supports multiple API formats:`);
console.log(` • OpenAI-compatible: /v1/chat/completions, /v1/models`);
console.log(` • OpenAI-compatible: /v1/chat/completions, /v1/responses, /v1/models`);
console.log(` • Gemini-compatible: /v1beta/models, /v1beta/models/{model}:generateContent`);
console.log(` • Claude-compatible: /v1/messages`);
console.log(` • Health check: /health`);

View file

@ -3,7 +3,7 @@ import * as path from 'path';
import * as http from 'http'; // Add http for IncomingMessage and ServerResponse types
import * as crypto from 'crypto'; // Import crypto for MD5 hashing
import { ApiServiceAdapter } from './adapter.js'; // Import ApiServiceAdapter
import { convertData, getOpenAIStreamChunkStop } from './convert.js';
import { convertData, getOpenAIStreamChunkStop, getOpenAIResponsesStreamChunkBegin, getOpenAIResponsesStreamChunkEnd } from './convert.js';
import { ProviderStrategyFactory } from './provider-strategies.js';
export const API_ACTIONS = {
@ -15,6 +15,7 @@ export const MODEL_PROTOCOL_PREFIX = {
// Model provider constants
GEMINI: 'gemini',
OPENAI: 'openai',
OPENAI_RESPONSES: 'openairesp',
CLAUDE: 'claude',
}
@ -22,6 +23,7 @@ export const MODEL_PROVIDER = {
// Model provider constants
GEMINI_CLI: 'gemini-cli-oauth',
OPENAI_CUSTOM: 'openai-custom',
OPENAI_CUSTOM_RESPONSES: 'openaiResponses-custom',
CLAUDE_CUSTOM: 'claude-custom',
KIRO_API: 'claude-kiro-oauth',
QWEN_API: 'openai-qwen-oauth',
@ -43,6 +45,7 @@ export function getProtocolPrefix(provider) {
export const ENDPOINT_TYPE = {
OPENAI_CHAT: 'openai_chat',
OPENAI_RESPONSES: 'openai_responses',
GEMINI_CONTENT: 'gemini_content',
CLAUDE_MESSAGE: 'claude_message',
OPENAI_MODEL_LIST: 'openai_model_list',
@ -212,10 +215,18 @@ export async function handleStreamRequest(res, service, model, requestBody, from
// The service returns a stream in its native format (toProvider).
const nativeStream = await service.generateContentStream(model, requestBody);
const needsConversion = getProtocolPrefix(fromProvider) !== getProtocolPrefix(toProvider);
const addEvent = getProtocolPrefix(fromProvider) === MODEL_PROTOCOL_PREFIX.CLAUDE;
const openStop = getProtocolPrefix(fromProvider) === MODEL_PROTOCOL_PREFIX.OPENAI;
const addEvent = getProtocolPrefix(fromProvider) === MODEL_PROTOCOL_PREFIX.CLAUDE || getProtocolPrefix(fromProvider) === MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES;
const openStop = getProtocolPrefix(fromProvider) === MODEL_PROTOCOL_PREFIX.OPENAI ;
const openResponses = getProtocolPrefix(fromProvider) === MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES ;
try {
if (openResponses && needsConversion) {
const beginChunks = getOpenAIResponsesStreamChunkBegin(model);
for (const chunk of beginChunks) {
res.write(`event: ${chunk.type}\n`);
res.write(`data: ${JSON.stringify(chunk)}\n\n`);
}
}
for await (const nativeChunk of nativeStream) {
// Convert chunk to the client's format (fromProvider), if necessary.
const chunkText = extractResponseText(nativeChunk, toProvider);
@ -223,7 +234,7 @@ export async function handleStreamRequest(res, service, model, requestBody, from
fullResponseText += chunkText;
}
const chunkToSend = needsConversion
const chunkToSend = needsConversion
? convertData(chunkText, 'streamChunk', toProvider, fromProvider, model)
: nativeChunk;
@ -231,15 +242,28 @@ export async function handleStreamRequest(res, service, model, requestBody, from
continue;
}
if (addEvent) {
res.write(`event: ${chunkToSend.type}\n`);
// console.log(`event: ${chunkToSend.type}\n`);
}
// 处理 chunkToSend 可能是数组或对象的情况
const chunksToSend = Array.isArray(chunkToSend) ? chunkToSend : [chunkToSend];
// fullOldResponseJson += JSON.stringify(nativeChunk)+"\n";
// fullResponseJson += JSON.stringify(chunkToSend)+"\n";
res.write(`data: ${JSON.stringify(chunkToSend)}\n\n`);
// console.log(`data: ${JSON.stringify(chunkToSend)}\n`);
for (const chunk of chunksToSend) {
if (addEvent) {
// fullResponseJson += chunk.type+"\n";
res.write(`event: ${chunk.type}\n`);
// console.log(`event: ${chunk.type}\n`);
}
// fullOldResponseJson += JSON.stringify(chunk)+"\n";
// fullResponseJson += JSON.stringify(chunk)+"\n\n";
res.write(`data: ${JSON.stringify(chunk)}\n\n`);
// console.log(`data: ${JSON.stringify(chunk)}\n`);
}
}
if (openResponses && needsConversion) {
const endChunks = getOpenAIResponsesStreamChunkEnd(model);
for (const chunk of endChunks) {
res.write(`event: ${chunk.type}\n`);
res.write(`data: ${JSON.stringify(chunk)}\n\n`);
}
}
if (openStop && needsConversion) {
res.write(`data: ${JSON.stringify(getOpenAIStreamChunkStop(model))}\n\n`);
@ -261,7 +285,7 @@ export async function handleStreamRequest(res, service, model, requestBody, from
res.end(JSON.stringify(errorPayload));
responseClosed = true;
}
} finally {
if (!responseClosed) {
res.end();
@ -296,7 +320,7 @@ export async function handleUnaryRequest(res, service, model, requestBody, fromP
uuid: pooluuid
});
}
// 返回错误响应给客户端
const errorResponse = {
error: {
@ -339,7 +363,7 @@ export async function handleModelListRequest(req, res, service, endpointType, CO
// 2. Convert the model list to the client's expected format, if necessary.
let clientModelList = nativeModelList;
if (getProtocolPrefix(fromProvider) !== getProtocolPrefix(toProvider)) {
if (getProtocolPrefix(fromProvider).includes(getProtocolPrefix(toProvider))) {
console.log(`[ModelList Convert] Converting model list from ${toProvider} to ${fromProvider}`);
clientModelList = convertData(nativeModelList, 'modelList', toProvider, fromProvider);
} else {
@ -379,6 +403,7 @@ export async function handleContentGenerationRequest(req, res, service, endpoint
const clientProviderMap = {
[ENDPOINT_TYPE.OPENAI_CHAT]: MODEL_PROTOCOL_PREFIX.OPENAI,
[ENDPOINT_TYPE.OPENAI_RESPONSES]: MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES,
[ENDPOINT_TYPE.CLAUDE_MESSAGE]: MODEL_PROTOCOL_PREFIX.CLAUDE,
[ENDPOINT_TYPE.GEMINI_CONTENT]: MODEL_PROTOCOL_PREFIX.GEMINI,
};

View file

@ -1,5 +1,17 @@
import { v4 as uuidv4 } from 'uuid';
import { MODEL_PROTOCOL_PREFIX, getProtocolPrefix } from './common.js';
import {
streamStateManager,
generateResponseCreated,
generateResponseInProgress,
generateOutputItemAdded,
generateContentPartAdded,
generateOutputTextDelta,
generateOutputTextDone,
generateContentPartDone,
generateOutputItemDone,
generateResponseCompleted
} from './openai/openai-responses-core.mjs';
// =============================================================================
// 常量和辅助函数定义
@ -178,10 +190,12 @@ export function convertData(data, type, fromProvider, toProvider, model) {
},
[MODEL_PROTOCOL_PREFIX.CLAUDE]: { // to Claude protocol
[MODEL_PROTOCOL_PREFIX.OPENAI]: toClaudeRequestFromOpenAI, // from OpenAI protocol
[MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES]: toClaudeRequestFromOpenAIResponses, // from OpenAI protocol (Responses format)
},
[MODEL_PROTOCOL_PREFIX.GEMINI]: { // to Gemini protocol
[MODEL_PROTOCOL_PREFIX.OPENAI]: toGeminiRequestFromOpenAI, // from OpenAI protocol
[MODEL_PROTOCOL_PREFIX.CLAUDE]: toGeminiRequestFromClaude, // from Claude protocol
[MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES]: toGeminiRequestFromOpenAIResponses, // from OpenAI protocol (Responses format)
},
},
response: {
@ -193,6 +207,10 @@ export function convertData(data, type, fromProvider, toProvider, model) {
[MODEL_PROTOCOL_PREFIX.GEMINI]: toClaudeChatCompletionFromGemini, // from Gemini protocol
[MODEL_PROTOCOL_PREFIX.OPENAI]: toClaudeChatCompletionFromOpenAI, // from OpenAI protocol
},
[MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES]: { // to OpenAI protocol (Responses format)
[MODEL_PROTOCOL_PREFIX.GEMINI]: toOpenAIResponsesFromGemini, // from Gemini protocol
[MODEL_PROTOCOL_PREFIX.CLAUDE]: toOpenAIResponsesFromClaude, // from Claude protocol
},
},
streamChunk: {
[MODEL_PROTOCOL_PREFIX.OPENAI]: { // to OpenAI protocol
@ -203,6 +221,10 @@ export function convertData(data, type, fromProvider, toProvider, model) {
[MODEL_PROTOCOL_PREFIX.GEMINI]: toClaudeStreamChunkFromGemini, // from Gemini protocol
[MODEL_PROTOCOL_PREFIX.OPENAI]: toClaudeStreamChunkFromOpenAI, // from OpenAI protocol
},
[MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES]: { // to OpenAI protocol (Responses format)
[MODEL_PROTOCOL_PREFIX.GEMINI]: toOpenAIResponsesStreamChunkFromGemini, // from Gemini protocol
[MODEL_PROTOCOL_PREFIX.CLAUDE]: toOpenAIResponsesStreamChunkFromClaude, // from Claude protocol
},
},
modelList: {
[MODEL_PROTOCOL_PREFIX.OPENAI]: { // to OpenAI protocol
@ -220,7 +242,7 @@ export function convertData(data, type, fromProvider, toProvider, model) {
if (!targetConversions) {
throw new Error(`Unsupported conversion type: ${type}`);
}
const toConversions = targetConversions[getProtocolPrefix(toProvider)];
if (!toConversions) {
throw new Error(`No conversions defined for target protocol: ${getProtocolPrefix(toProvider)} for type: ${type}`);
@ -228,9 +250,9 @@ export function convertData(data, type, fromProvider, toProvider, model) {
const conversionFunction = toConversions[getProtocolPrefix(fromProvider)];
if (!conversionFunction) {
throw new Error(`No conversion function found from ${fromProvider} to ${toProvider} for type: ${type}`);
throw new Error(`No conversion function found from ${getProtocolPrefix(fromProvider)} to ${toProvider} for type: ${type}`);
}
console.log(conversionFunction);
if (type === 'response' || type === 'streamChunk' || type === 'modelList') {
return conversionFunction(data, model);
@ -288,6 +310,7 @@ export function toOpenAIRequestFromGemini(geminiRequest) {
return openaiRequest;
}
/**
* Processes Gemini parts to OpenAI content format with multimodal support.
* @param {Array} parts - Array of Gemini parts.
@ -579,33 +602,6 @@ export function toOpenAIStreamChunkFromClaude(claudeChunk, model) {
};
}
export function getOpenAIStreamChunkStop(model) {
return {
id: `chatcmpl-${uuidv4()}`, // uuidv4 needs to be imported or handled
object: "chat.completion.chunk",
created: Math.floor(Date.now() / 1000),
model: model,
system_fingerprint: "",
choices: [{
index: 0,
delta: {
content: "",
reasoning_content: ""
},
finish_reason: 'stop',
message: {
content: "",
reasoning_content: ""
}
}],
usage:{
prompt_tokens: 0,
completion_tokens: 0,
total_tokens: 0,
},
};
}
/**
* Converts a Claude API model list response to an OpenAI model list response.
* @param {Array<Object>} claudeModels - The array of model objects from Claude API.
@ -784,7 +780,7 @@ export function toOpenAIRequestFromClaude(claudeRequest) {
});
}
}
// ---------------- OpenAI 兼容性校验 ----------------
// 确保所有 assistant.tool_calls 均有后续 tool 响应消息;否则移除不匹配的 tool_call
const validatedMessages = [];
@ -884,7 +880,7 @@ export function toOpenAIRequestFromClaude(claudeRequest) {
console.info(`Budget tokens: ${budgetTokens} -> reasoning_effort: '${reasoningEffort}'`);
}
}
// Add system message at the beginning if present
if (systemMessageContent) {
let stringifiedSystemMessageContent = systemMessageContent;
@ -898,6 +894,7 @@ export function toOpenAIRequestFromClaude(claudeRequest) {
return openaiRequest;
}
/**
* Processes Claude content to OpenAI content format with multimodal support.
* @param {Array} content - Array of Claude content blocks.
@ -1023,13 +1020,31 @@ export function toGeminiRequestFromOpenAI(openaiRequest) {
if (systemInstruction) geminiRequest.systemInstruction = systemInstruction;
// Handle tools and tool_choice
// Handle tools
if (openaiRequest.tools?.length) {
geminiRequest.tools = openaiRequest.tools.map(t => {
const tool = {};
tool[t.function.name] = t.function.parameters || {};
return tool;
});
geminiRequest.tools = [{
functionDeclarations: openaiRequest.tools.map(t => {
// Ensure tool is a valid object and has function property
if (!t || typeof t !== 'object' || !t.function) {
console.warn("Skipping invalid tool declaration in openaiRequest.tools.");
return null; // Return null for invalid tools, filter out later
}
const func = t.function;
// Clean parameters schema for Gemini compatibility
const parameters = _cleanJsonSchemaProperties(func.parameters || {});
return {
name: String(func.name || ''), // Ensure name is string
description: String(func.description || ''), // Ensure description is string
parameters: parameters // Use cleaned parameters
};
}).filter(Boolean) // Filter out any nulls from invalid tool declarations
}];
// If no valid functionDeclarations, remove the tools array
if (geminiRequest.tools[0].functionDeclarations.length === 0) {
delete geminiRequest.tools;
}
}
if (openaiRequest.tool_choice) {
@ -1991,3 +2006,566 @@ export function toClaudeStreamChunkFromGemini(geminiChunk, model) {
return null;
}
/**
* Converts a Claude API response to an OpenAI Responses API response.
* @param {Object} claudeResponse - The Claude API response object.
* @param {string} model - The model name to include in the response.
* @returns {Object} The formatted OpenAI Responses API response.
*/
export function toOpenAIResponsesFromClaude(claudeResponse, model) {
// 根据参考示例重构响应结构
const content = processClaudeResponseContent(claudeResponse.content);
const textContent = typeof content === 'string' ? content : JSON.stringify(content);
// 将claude的内容转换为OpenAI Responses输出格式
let output = [];
// 添加文本内容
output.push({
type: "message",
id: `msg_${uuidv4().replace(/-/g, '')}`,
summary: [],
type: "message",
role: "assistant",
status: "completed",
content: [{
annotations: [],
logprobs: [],
text: textContent,
type: "output_text"
}]
});
return {
background: false,
created_at: Math.floor(Date.now() / 1000),
error: null,
id: `resp_${uuidv4().replace(/-/g, '')}`,
incomplete_details: null,
max_output_tokens: null,
max_tool_calls: null,
metadata: {},
model: model || claudeResponse.model,
object: "response",
output: output,
parallel_tool_calls: true,
previous_response_id: null,
prompt_cache_key: null,
reasoning: {
// effort: "minimal",
// summary: "detailed"
},
safety_identifier: "user-"+uuidv4().replace(/-/g, ''), // 示例值
service_tier: "default",
status: "completed",
store: false,
temperature: 1,
text: {
format: {type: "text"},
// verbosity: "medium"
},
tool_choice: "auto",
tools: [],
top_logprobs: 0,
top_p: 1,
truncation: "disabled",
usage: {
input_tokens: claudeResponse.usage?.input_tokens || 0, // 示例值
input_tokens_details: {
cached_tokens: claudeResponse.usage?.cache_creation_input_tokens || 0, // 如果有缓存相关数据则使用
},
output_tokens: claudeResponse.usage?.output_tokens || 0, // 示例值
output_tokens_details: {
reasoning_tokens: 0
},
total_tokens: (claudeResponse.usage?.input_tokens || 0) + (claudeResponse.usage?.output_tokens || 0) // 示例值
},
user: null
};
}
/**
* Converts a Gemini API response to an OpenAI Responses API response.
* @param {Object} geminiResponse - The Gemini API response object.
* @param {string} model - The model name to include in the response.
* @returns {Object} The formatted OpenAI Responses API response.
*/
export function toOpenAIResponsesFromGemini(geminiResponse, model) {
// 根据参考示例重构响应结构
const content = processGeminiResponseContent(geminiResponse);
const textContent = typeof content === 'string' ? content : JSON.stringify(content);
// 将gemini的内容转换为OpenAI Responses输出格式
let output = [];
// 添加文本内容
output.push({
id: `msg_${uuidv4().replace(/-/g, '')}`,
summary: [],
type: "message",
role: "assistant",
status: "completed",
content: [{
annotations: [],
logprobs: [],
text: textContent,
type: "output_text"
}]
});
return {
background: false,
created_at: Math.floor(Date.now() / 1000),
error: null,
id: `resp_${uuidv4().replace(/-/g, '')}`,
incomplete_details: null,
max_output_tokens: null,
max_tool_calls: null,
metadata: {},
model: model,
object: "response",
output: output,
parallel_tool_calls: true,
previous_response_id: null,
prompt_cache_key: null,
reasoning: {
// effort: "minimal",
// summary: "detailed"
},
safety_identifier: "user-"+uuidv4().replace(/-/g, ''), // 示例值
service_tier: "default",
status: "completed",
store: false,
temperature: 1,
text: {
format: {type: "text"},
// verbosity: "medium"
},
tool_choice: "auto",
tools: [],
top_logprobs: 0,
top_p: 1,
truncation: "disabled",
usage: {
input_tokens: geminiResponse.usageMetadata?.promptTokenCount || 0, // 示例值
input_tokens_details: {
cached_tokens: geminiResponse.usageMetadata?.cachedTokens || 0, // 使用正确的Gemini缓存字段
},
output_tokens: geminiResponse.usageMetadata?.candidatesTokenCount || 0, // 示例值
output_tokens_details: {
reasoning_tokens: 0
},
total_tokens: geminiResponse.usageMetadata?.totalTokenCount || 0, // 示例值
},
user: null
};
}
/**
* Converts an OpenAI Responses API request body to a Claude API request body.
* @param {Object} responsesRequest - The request body from the OpenAI Responses API.
* @returns {Object} The formatted request body for the Claude API.
*/
export function toClaudeRequestFromOpenAIResponses(responsesRequest) {
// The OpenAI Responses API uses input and instructions instead of messages
const claudeRequest = {
model: responsesRequest.model,
max_tokens: checkAndAssignOrDefault(responsesRequest.max_tokens, DEFAULT_MAX_TOKENS),
temperature: checkAndAssignOrDefault(responsesRequest.temperature, DEFAULT_TEMPERATURE),
top_p: checkAndAssignOrDefault(responsesRequest.top_p, DEFAULT_TOP_P),
};
// Process instructions as system message
if (responsesRequest.instructions) {
claudeRequest.system = [];
claudeRequest.system.push({
text: typeof responsesRequest.instructions === 'string' ? responsesRequest.instructions : JSON.stringify(responsesRequest.instructions)
});
}
const claudeMessages = [];
// Process input as user message content
if (responsesRequest.input) {
if (typeof responsesRequest.input === 'string') {
// Create user message with the string content
claudeMessages.push({
role: 'user',
content: [{
type: 'text',
text: responsesRequest.input
}]
});
} else {
// Handle array of messages or items - process the entire array
for (const message of responsesRequest.input) {
const role = message.role === 'assistant' ? 'assistant' : 'user';
let content = [];
if (message.role === 'tool') {
// Claude expects tool_result to be in a 'user' message
// The content of a tool message is a single tool_result block
content.push({
type: 'tool_result',
tool_use_id: message.tool_call_id, // Use tool_call_id from OpenAI tool message
content: safeParseJSON(message.content) // Parse content as JSON if possible
});
claudeMessages.push({ role: 'user', content: content });
} else if (message.role === 'assistant' && message.tool_calls?.length) {
// Assistant message with tool calls - properly format as tool_use blocks
// Claude expects tool_use to be in an 'assistant' message
const toolUseBlocks = message.tool_calls.map(tc => ({
type: 'tool_use',
id: tc.id,
name: tc.function.name,
input: safeParseJSON(tc.function.arguments)
}));
claudeMessages.push({ role: 'assistant', content: toolUseBlocks });
} else {
// Regular user or assistant message (text and multimodal)
if (typeof message.content === 'string') {
if (message.content) {
content.push({ type: 'text', text: message.content });
}
} else if (Array.isArray(message.content)) {
message.content.forEach(item => {
if (!item) return;
switch (item.type) {
case 'input_text':
if (item.text) {
content.push({ type: 'text', text: item.text });
}
break;
case 'output_text':
if (item.text) {
content.push({ type: 'text', text: item.text });
}
break;
case 'image_url':
if (item.image_url) {
const imageUrl = typeof item.image_url === 'string'
? item.image_url
: item.image_url.url;
if (imageUrl.startsWith('data:')) {
const [header, data] = imageUrl.split(',');
const mediaType = header.match(/data:([^;]+)/)?.[1] || 'image/jpeg';
content.push({
type: 'image',
source: {
type: 'base64',
media_type: mediaType,
data: data
}
});
} else {
// Claude requires base64 for images, so for URLs, we'll represent as text
content.push({ type: 'text', text: `[Image: ${imageUrl}]` });
}
}
break;
case 'audio':
// Handle audio content as text placeholder
if (item.audio_url) {
const audioUrl = typeof item.audio_url === 'string'
? item.audio_url
: item.audio_url.url;
content.push({ type: 'text', text: `[Audio: ${audioUrl}]` });
}
break;
}
});
}
// Only add message if content is not empty
if (content.length > 0) {
claudeMessages.push({ role: role, content: content });
}
}
}
}
}
// Process tools if present
// if (responsesRequest.tools && Array.isArray(responsesRequest.tools)) {
// claudeRequest.tools = responsesRequest.tools.map(tool => ({
// name: tool.name,
// description: tool.description || '',
// input_schema: tool.parameters || { type: 'object', properties: {} }
// }));
// claudeRequest.tool_choice = buildClaudeToolChoice(responsesRequest.tool_choice);
// }
// Process messages
claudeRequest.messages = claudeMessages;
claudeRequest.stream = responsesRequest.stream || false;
return claudeRequest;
}
/**
* Converts an OpenAI Responses API request body to a Gemini API request body.
* @param {Object} responsesRequest - The request body from the OpenAI Responses API.
* @returns {Object} The formatted request body for the Gemini API.
*/
export function toGeminiRequestFromOpenAIResponses(responsesRequest) {
// The OpenAI Responses API uses input and instructions instead of messages
const geminiRequest = {
contents: []
};
// Process instructions as system instruction
if (responsesRequest.instructions) {
let instructionsText = '';
if (typeof responsesRequest.instructions === 'string') {
instructionsText = responsesRequest.instructions;
} else {
instructionsText = JSON.stringify(responsesRequest.instructions);
}
geminiRequest.systemInstruction = {
parts: [{ text: instructionsText }]
};
}
// Process input as user content
if (responsesRequest.input) {
let inputContent = '';
if (typeof responsesRequest.input === 'string') {
inputContent = responsesRequest.input;
} else if (Array.isArray(responsesRequest.input)) {
// Handle array of messages or items
if (responsesRequest.input.length > 0) {
// For compatibility, take the content of the last item with text content
const lastInputItem = [...responsesRequest.input].reverse().find(item =>
item && (
(item.content && typeof item.content === 'string') ||
(item.content && Array.isArray(item.content) && item.content.some(c => c && c.text)) ||
(item.role === 'user' && item.content)
)
);
if (lastInputItem) {
if (typeof lastInputItem.content === 'string') {
inputContent = lastInputItem.content;
} else if (Array.isArray(lastInputItem.content)) {
// Process array of content blocks
inputContent = lastInputItem.content
.filter(block => block && block.text)
.map(block => block.text)
.join(' ');
} else {
// General fallback
inputContent = JSON.stringify(lastInputItem.content || lastInputItem);
}
}
}
}
if (inputContent) {
// Add user message with the input content
geminiRequest.contents.push({
role: 'user',
parts: [{ text: inputContent }]
});
}
} else {
// If no input is provided, ensure we have at least one user message for Gemini
geminiRequest.contents.push({
role: 'user',
parts: [{ text: 'Hello' }] // Default content to satisfy Gemini API requirement
});
}
// Add generation config
const generationConfig = {};
generationConfig.maxOutputTokens = checkAndAssignOrDefault(responsesRequest.max_tokens, DEFAULT_GEMINI_MAX_TOKENS);
generationConfig.temperature = checkAndAssignOrDefault(responsesRequest.temperature, DEFAULT_TEMPERATURE);
generationConfig.topP = checkAndAssignOrDefault(responsesRequest.top_p, DEFAULT_TOP_P);
if (Object.keys(generationConfig).length > 0) {
geminiRequest.generationConfig = generationConfig;
}
// Process tools if present
if (responsesRequest.tools && Array.isArray(responsesRequest.tools)) {
geminiRequest.tools = [{
functionDeclarations: responsesRequest.tools
.filter(tool => tool && (tool.type === 'function' || tool.function))
.map(tool => {
const func = tool.function || tool;
return {
name: String(func.name || tool.name || ''),
description: String(func.description || tool.description || ''),
parameters: func.parameters || tool.parameters || { type: 'object', properties: {} }
};
}).filter(Boolean) // Filter out any invalid tools
}];
// If no valid functionDeclarations, remove the tools array
if (geminiRequest.tools[0].functionDeclarations.length === 0) {
delete geminiRequest.tools;
}
}
return geminiRequest;
}
/**
* Converts a Claude API stream chunk to an OpenAI Responses API stream chunk.
* @param {Object} claudeChunk - The Claude API stream chunk object.
* @param {string} [model] - Optional model name to include in the response.
* @param {string} [requestId] - Optional request ID to maintain stream state across chunks.
* @returns {Array} The formatted OpenAI Responses API stream chunks as an array of events.
*/
export function toOpenAIResponsesStreamChunkFromClaude(claudeChunk, model, requestId = null) {
if (!claudeChunk) {
return [];
}
// 如果没有提供requestId则生成一个首次调用时
const id = requestId || Date.now().toString();
// 设置模型信息(仅在新请求时设置)
if (!requestId) {
streamStateManager.setModel(id, model);
}
// Handle text content from Claude stream
let content = '';
if (typeof claudeChunk === 'string') {
content = claudeChunk;
} else if (claudeChunk && typeof claudeChunk === 'object' && claudeChunk.delta?.text) {
content = claudeChunk.delta.text;
} else if (claudeChunk && typeof claudeChunk === 'object') {
content = claudeChunk;
}
// 对于第一个数据块fullText为空生成开始事件
const state = streamStateManager.getOrCreateState(id);
if (state.fullText === '' && !requestId) { // 只在首次调用时未指定requestId时生成开始事件
// 在这种情况下,我们需要先添加内容到状态
state.fullText = content;
return [
// ...getOpenAIResponsesStreamChunkBegin(id, model),
generateOutputTextDelta(id, content),
// ...getOpenAIResponsesStreamChunkEnd(id)
];
} else if (content === '') {
// 如果是结束块,生成结束事件
const doneEvents = getOpenAIResponsesStreamChunkEnd(id);
// 清理状态
streamStateManager.cleanup(id);
return doneEvents;
} else {
// 中间数据块只返回delta事件但也要更新状态
streamStateManager.updateText(id, content);
return [
generateOutputTextDelta(id, content)
];
}
}
/**
* Converts a Gemini API stream chunk to an OpenAI Responses API stream chunk.
* @param {Object} geminiChunk - The Gemini API stream chunk object.
* @param {string} [model] - Optional model name to include in the response.
* @param {string} [requestId] - Optional request ID to maintain stream state across chunks.
* @returns {Array} The formatted OpenAI Responses API stream chunks as an array of events.
*/
export function toOpenAIResponsesStreamChunkFromGemini(geminiChunk, model, requestId = null) {
if (!geminiChunk) {
return [];
}
// 如果没有提供requestId则生成一个首次调用时
const id = requestId || Date.now().toString();
// 设置模型信息(仅在新请求时设置)
if (!requestId) {
streamStateManager.setModel(id, model);
}
// Handle text content in stream
let content = '';
if (typeof geminiChunk === 'string') {
content = geminiChunk;
} else if (geminiChunk && typeof geminiChunk === 'object') {
// Extract content from Gemini chunk if it's an object
content = geminiChunk.content || geminiChunk.text || geminiChunk;
}
// 对于第一个数据块fullText为空生成开始事件
const state = streamStateManager.getOrCreateState(id);
if (state.fullText === '' && !requestId) { // 只在首次调用时未指定requestId时生成开始事件
// 在这种情况下,我们需要先添加内容到状态
state.fullText = content;
return [
// ...getOpenAIResponsesStreamChunkBegin(id, model),
generateOutputTextDelta(id, content),
// ...getOpenAIResponsesStreamChunkEnd(id)
];
} else if (content === '') {
// 如果是结束块,生成结束事件
const doneEvents = getOpenAIResponsesStreamChunkEnd(id);
// 清理状态
streamStateManager.cleanup(id);
return doneEvents;
} else {
// 中间数据块只返回delta事件但也要更新状态
streamStateManager.updateText(id, content);
return [
generateOutputTextDelta(id, content)
];
}
}
export function getOpenAIStreamChunkStop(model) {
return {
id: `chatcmpl-${uuidv4()}`, // uuidv4 needs to be imported or handled
object: "chat.completion.chunk",
created: Math.floor(Date.now() / 1000),
model: model,
system_fingerprint: "",
choices: [{
index: 0,
delta: {
content: "",
reasoning_content: ""
},
finish_reason: 'stop',
message: {
content: "",
reasoning_content: ""
}
}],
usage:{
prompt_tokens: 0,
completion_tokens: 0,
total_tokens: 0,
},
};
}
export function getOpenAIResponsesStreamChunkBegin(id, model){
return [
generateResponseCreated(id, model),
generateResponseInProgress(id),
generateOutputItemAdded(id),
generateContentPartAdded(id)
];
}
export function getOpenAIResponsesStreamChunkEnd(id){
return [
generateOutputTextDone(id),
generateContentPartDone(id),
generateOutputItemDone(id),
generateResponseCompleted(id)
];
}

View file

@ -0,0 +1,146 @@
import axios from 'axios';
// OpenAI Responses API specification service for interacting with third-party models
export class OpenAIResponsesApiService {
constructor(config) {
if (!config.OPENAI_API_KEY) {
throw new Error("OpenAI API Key is required for OpenAIResponsesApiService.");
}
this.config = config;
this.apiKey = config.OPENAI_API_KEY;
this.baseUrl = config.OPENAI_BASE_URL || 'https://api.openai.com/v1';
console.log(`[OpenAIResponsesApiService] Base URL: ${JSON.stringify(config)}`);
this.axiosInstance = axios.create({
baseURL: this.baseUrl,
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.apiKey}`
}
});
}
async callApi(endpoint, body, isRetry = false, retryCount = 0) {
const maxRetries = this.config.REQUEST_MAX_RETRIES || 3;
const baseDelay = this.config.REQUEST_BASE_DELAY || 1000; // 1 second base delay
try {
const response = await this.axiosInstance.post(endpoint, body);
return response.data;
} catch (error) {
const status = error.response?.status;
const data = error.response?.data;
if (status === 401 || status === 403) {
console.error(`[API] Received ${status}. API Key might be invalid or expired.`);
throw error;
}
// Handle 429 (Too Many Requests) with exponential backoff
if (status === 429 && retryCount < maxRetries) {
const delay = baseDelay * Math.pow(2, retryCount);
console.log(`[API] Received 429 (Too Many Requests). Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`);
await new Promise(resolve => setTimeout(resolve, delay));
return this.callApi(endpoint, body, isRetry, retryCount + 1);
}
// Handle other retryable errors (5xx server errors)
if (status >= 500 && status < 600 && retryCount < maxRetries) {
const delay = baseDelay * Math.pow(2, retryCount);
console.log(`[API] Received ${status} server error. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`);
await new Promise(resolve => setTimeout(resolve, delay));
return this.callApi(endpoint, body, isRetry, retryCount + 1);
}
console.error(`Error calling OpenAI Responses API (Status: ${status}):`, data || error.message);
throw error;
}
}
async *streamApi(endpoint, body, isRetry = false, retryCount = 0) {
const maxRetries = this.config.REQUEST_MAX_RETRIES || 3;
const baseDelay = this.config.REQUEST_BASE_DELAY || 1000; // 1 second base delay
// OpenAI 的流式请求需要将 stream 设置为 true
const streamRequestBody = { ...body, stream: true };
try {
const response = await this.axiosInstance.post(endpoint, streamRequestBody, {
responseType: 'stream'
});
const stream = response.data;
let buffer = '';
for await (const chunk of stream) {
buffer += chunk.toString();
let newlineIndex;
while ((newlineIndex = buffer.indexOf('\n')) !== -1) {
const line = buffer.substring(0, newlineIndex).trim();
buffer = buffer.substring(newlineIndex + 1);
if (line.startsWith('data: ')) {
const jsonData = line.substring(6).trim();
if (jsonData === '[DONE]') {
return; // Stream finished
}
try {
const parsedChunk = JSON.parse(jsonData);
yield parsedChunk;
} catch (e) {
console.warn("[OpenAIResponsesApiService] Failed to parse stream chunk JSON:", e.message, "Data:", jsonData);
}
} else if (line === '') {
// Empty line, end of an event
}
}
}
} catch (error) {
const status = error.response?.status;
const data = error.response?.data;
if (status === 401 || status === 403) {
console.error(`[API] Received ${status} during stream. API Key might be invalid or expired.`);
throw error;
}
// Handle 429 (Too Many Requests) with exponential backoff
if (status === 429 && retryCount < maxRetries) {
const delay = baseDelay * Math.pow(2, retryCount);
console.log(`[API] Received 429 (Too Many Requests) during stream. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`);
await new Promise(resolve => setTimeout(resolve, delay));
yield* this.streamApi(endpoint, body, isRetry, retryCount + 1);
return;
}
// Handle other retryable errors (5xx server errors)
if (status >= 500 && status < 600 && retryCount < maxRetries) {
const delay = baseDelay * Math.pow(2, retryCount);
console.log(`[API] Received ${status} server error during stream. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`);
await new Promise(resolve => setTimeout(resolve, delay));
yield* this.streamApi(endpoint, body, isRetry, retryCount + 1);
return;
}
console.error(`Error calling OpenAI Responses streaming API (Status: ${status}):`, data || error.message);
throw error;
}
}
async generateContent(model, requestBody) {
return this.callApi('/responses', requestBody);
}
async *generateContentStream(model, requestBody) {
yield* this.streamApi('/responses', requestBody);
}
async listModels() {
try {
const response = await this.axiosInstance.get('/models');
return response.data;
} catch (error) {
const status = error.response?.status;
const data = error.response?.data;
console.error(`Error listing OpenAI Responses models (Status: ${status}):`, data || error.message);
throw error;
}
}
}

View file

@ -0,0 +1,329 @@
import { v4 as uuidv4 } from 'uuid';
// 流式处理状态管理
class StreamState {
constructor() {
this.states = new Map(); // 使用Map存储不同请求的状态
}
// 获取或创建状态
getOrCreateState(requestId) {
if (!this.states.has(requestId)) {
this.states.set(requestId, {
id: `resp_${uuidv4().replace(/-/g, '')}`,
msgId: `msg_${uuidv4().replace(/-/g, '')}`,
fullText: '',
sequenceNumber: 0,
model: null,
status: 'in_progress',
startTime: Math.floor(Date.now() / 1000)
});
}
return this.states.get(requestId);
}
// 更新文本内容
updateText(requestId, textDelta) {
const state = this.getOrCreateState(requestId);
state.fullText += textDelta;
state.sequenceNumber += 1;
return state;
}
// 设置模型信息
setModel(requestId, model) {
const state = this.getOrCreateState(requestId);
state.model = model;
return state;
}
// 完成请求
completeRequest(requestId) {
const state = this.getOrCreateState(requestId);
state.status = 'completed';
return state;
}
// 清理状态
cleanup(requestId) {
this.states.delete(requestId);
}
}
// 创建全局流式状态管理器
const streamStateManager = new StreamState();
/**
* Generates a response.created event
*/
function generateResponseCreated(requestId, model) {
const state = streamStateManager.getOrCreateState(requestId);
if (model) {
state.model = model;
}
return {
type: 'response.created',
response: {
id: state.id,
object: 'response',
created_at: state.startTime,
status: 'in_progress',
error: null,
incomplete_details: null,
instructions: '',
max_output_tokens: null,
model: state.model || 'gpt-4.1-2025-04-14',
output: [],
parallel_tool_calls: true,
previous_response_id: null,
reasoning: { },
store: false,
temperature: 1,
text: { format: { type: "text" }},
tool_choice: "auto",
tools: [],
top_logprobs: 0,
top_p: 1,
truncation: "disabled",
usage: null,
user: null,
metadata: {}
}
};
}
/**
* Generates a response.in_progress event
*/
function generateResponseInProgress(requestId) {
const state = streamStateManager.getOrCreateState(requestId);
return {
type: 'response.in_progress',
response: {
id: state.id,
object: 'response',
created_at: state.startTime,
status: 'in_progress',
error: null,
incomplete_details: null,
instructions: '',
max_output_tokens: null,
model: state.model || 'gpt-4.1-2025-04-14',
output: [],
parallel_tool_calls: true,
previous_response_id: null,
reasoning: { },
service_tier: "auto",
store: false,
temperature: 1,
text: { format: { type: "text" }},
tool_choice: "auto",
tools: [],
top_logprobs: 0,
top_p: 1,
truncation: "disabled",
usage: null,
user: null,
metadata: {}
}
};
}
/**
* Generates a response.output_item.added event
*/
function generateOutputItemAdded(requestId) {
const state = streamStateManager.getOrCreateState(requestId);
return {
type: 'response.output_item.added',
output_index: 0,
item: {
id: state.msgId,
summary: [],
type: 'message',
role: 'assistant',
status: 'in_progress',
content: []
}
};
}
/**
* Generates a response.content_part.added event
*/
function generateContentPartAdded(requestId) {
const state = streamStateManager.getOrCreateState(requestId);
return {
type: 'response.content_part.added',
item_id: state.msgId,
output_index: 0,
content_index: 0,
part: {
type: 'output_text',
text: '',
annotations: [],
logprobs: []
}
};
}
/**
* Generates a response.output_text.delta event
*/
function generateOutputTextDelta(requestId, delta) {
const state = streamStateManager.getOrCreateState(requestId);
state.fullText += delta;
return {
type: 'response.output_text.delta',
item_id: state.msgId,
output_index: 0,
content_index: 0,
delta: delta,
logprobs: [],
obfuscation: null
};
}
/**
* Generates a response.output_text.done event
*/
function generateOutputTextDone(requestId) {
const state = streamStateManager.getOrCreateState(requestId);
return {
type: 'response.output_text.done',
item_id: state.msgId,
output_index: 0,
content_index: 0,
text: state.fullText,
logprobs: []
};
}
/**
* Generates a response.content_part.done event
*/
function generateContentPartDone(requestId) {
const state = streamStateManager.getOrCreateState(requestId);
return {
type: 'response.content_part.done',
item_id: state.msgId,
output_index: 0,
content_index: 0,
part: {
type: 'output_text',
text: state.fullText,
annotations: [],
logprobs: []
}
};
}
/**
* Generates a response.output_item.done event
*/
function generateOutputItemDone(requestId) {
const state = streamStateManager.getOrCreateState(requestId);
return {
type: 'response.output_item.done',
output_index: 0,
item: {
id: state.msgId,
summary: [],
type: 'message',
role: 'assistant',
status: 'completed',
content: [
{
type: 'output_text',
text: state.fullText,
annotations: [],
logprobs: []
}
]
}
};
}
/**
* Generates a response.completed event
*/
function generateResponseCompleted(requestId, usage) {
const state = streamStateManager.getOrCreateState(requestId);
return {
type: 'response.completed',
response: {
background: false,
created_at: state.startTime,
error: null,
id: state.id,
incomplete_details: null,
max_output_tokens: null,
max_tool_calls: null,
metadata: {},
model: state.model || 'gpt-4.1-2025-04-14',
object: 'response',
output: [
{
id: state.msgId,
summary: [],
type: 'message',
role: 'assistant',
status: 'completed',
content: [
{
type: 'output_text',
text: state.fullText,
annotations: [],
logprobs: []
}
]
}
],
parallel_tool_calls: true,
previous_response_id: null,
prompt_cache_key: null,
reasoning: {
},
safety_identifier: `user-${uuidv4().replace(/-/g, '')}`, // 随机值
service_tier: "default",
status: "completed",
store: false,
temperature: 1,
text: {
format: { type: "text" }
},
tool_choice: "auto",
tools: [],
top_logprobs: 0,
top_p: 1,
truncation: "disabled",
usage: usage || {
input_tokens: Math.floor(Math.random() * 100) + 20, // 随机值
input_tokens_details: {
cached_tokens: Math.floor(Math.random() * 50) // 随机值
},
output_tokens: state.fullText.split('').length,
output_tokens_details: {
reasoning_tokens: 0
},
total_tokens: Math.floor(Math.random() * 100) + 20 + state.fullText.split('').length // 随机值+文本长度
},
user: null
}
};
}
// 导出流式状态管理器以供外部使用
export { streamStateManager, generateResponseCreated, generateResponseInProgress,
generateOutputItemAdded, generateContentPartAdded, generateOutputTextDelta,
generateOutputTextDone, generateContentPartDone, generateOutputItemDone,
generateResponseCompleted };

View file

@ -0,0 +1,123 @@
import { ProviderStrategy } from '../provider-strategy.js';
import { extractSystemPromptFromRequestBody, MODEL_PROTOCOL_PREFIX } from '../common.js';
/**
* OpenAI Responses API strategy implementation.
* Migrated from Chat Completions API to Responses API.
*/
class ResponsesAPIStrategy extends ProviderStrategy {
extractModelAndStreamInfo(req, requestBody) {
const model = requestBody.model;
const isStream = requestBody.stream === true;
return { model, isStream };
}
extractResponseText(response) {
if (!response.output) {
return '';
}
// In Responses API, output is an array of items
for (const item of response.output) {
if (item.type === 'message' && item.content && item.content.length > 0) {
for (const content of item.content) {
if (content.type === 'output_text' && content.text) {
return content.text;
}
}
}
}
return '';
}
extractPromptText(requestBody) {
// In Responses API, input can be a string or array of items
if (typeof requestBody.input === 'string') {
return requestBody.input;
} else if (Array.isArray(requestBody.input)) {
// If input is an array of items/messages, get the last user content
const userInputItems = requestBody.input.filter(item =>
(item.role && item.role === 'user') ||
(item.type && item.type === 'message' && item.role === 'user') ||
(item.type && item.type === 'user')
);
if (userInputItems.length > 0) {
const lastInput = userInputItems[userInputItems.length - 1];
if (typeof lastInput.content === 'string') {
return lastInput.content;
} else if (Array.isArray(lastInput.content)) {
return lastInput.content.map(item => item.text || item.content || '').join('\n');
}
}
}
return '';
}
async applySystemPromptFromFile(config, requestBody) {
if (!config.SYSTEM_PROMPT_FILE_PATH) {
return requestBody;
}
const filePromptContent = config.SYSTEM_PROMPT_CONTENT;
if (filePromptContent === null) {
return requestBody;
}
// In Responses API, system instructions are typically passed in 'instructions' field
// or in the input array with role: 'system'
requestBody.instructions = requestBody.instructions || filePromptContent;
// If using instructions field is not desired, append to input array instead
if (!requestBody.instructions || config.SYSTEM_PROMPT_MODE === 'append') {
if (typeof requestBody.input === 'string') {
// Convert to array format to add system message
requestBody.input = [
{ role: 'system', content: filePromptContent },
{ role: 'user', content: requestBody.input }
];
} else if (Array.isArray(requestBody.input)) {
// Check if system message already exists
const systemMessageIndex = requestBody.input.findIndex(m =>
m.role === 'system' || (m.type && m.type === 'system')
);
if (systemMessageIndex !== -1) {
requestBody.input[systemMessageIndex].content = filePromptContent;
} else {
requestBody.input.unshift({ role: 'system', content: filePromptContent });
}
} else {
// If input is not defined, initialize with system message
requestBody.input = [{ role: 'system', content: filePromptContent }];
}
} else if (requestBody.instructions) {
// If system prompt mode is not append, then replace the instructions
requestBody.instructions = filePromptContent;
}
console.log(`[System Prompt] Applied system prompt from ${config.SYSTEM_PROMPT_FILE_PATH} in '${config.SYSTEM_PROMPT_MODE}' mode for provider 'responses'.`);
return requestBody;
}
async manageSystemPrompt(requestBody) {
// For Responses API, we may extract instructions or system messages from input
let incomingSystemText = '';
if (requestBody.instructions) {
incomingSystemText = requestBody.instructions;
} else if (Array.isArray(requestBody.input)) {
const systemMessage = requestBody.input.find(item =>
item.role === 'system' || (item.type && item.type === 'system')
);
if (systemMessage && systemMessage.content) {
incomingSystemText = systemMessage.content;
}
}
await this._updateSystemPromptFile(incomingSystemText, MODEL_PROTOCOL_PREFIX.OPENAI);
}
}
export { ResponsesAPIStrategy };

View file

@ -212,6 +212,9 @@ export class ProviderPoolManager {
case MODEL_PROVIDER.QWEN_API:
modelName = 'qwen3-coder-flash'; // Example model name for Qwen
break;
case MODEL_PROVIDER.OPENAI_CUSTOM_RESPONSES:
modelName = 'gpt-5-low'; // Example model name for OpenAI Custom Responses
break;
default:
console.warn(`[ProviderPoolManager] Unknown provider type for health check: ${providerType}`);
return false;
@ -226,6 +229,12 @@ export class ProviderPoolManager {
}]
};
if (providerType === MODEL_PROVIDER.OPENAI_CUSTOM_RESPONSES) {
healthCheckRequest.input = [{ role: 'user', content: 'Hello, are you ok?' }];
healthCheckRequest.model = modelName;
delete healthCheckRequest.contents;
}
// For OpenAI and Claude providers, we need a different request format
if (providerType === MODEL_PROVIDER.OPENAI_CUSTOM || providerType === MODEL_PROVIDER.CLAUDE_CUSTOM || providerType === MODEL_PROVIDER.KIRO_API || providerType === MODEL_PROVIDER.QWEN_API) {
healthCheckRequest.messages = [{ role: 'user', content: 'Hello, are you ok?' }];

View file

@ -2,6 +2,7 @@ import { MODEL_PROTOCOL_PREFIX } from './common.js';
import { GeminiStrategy } from './gemini/gemini-strategy.js';
import { OpenAIStrategy } from './openai/openai-strategy.js';
import { ClaudeStrategy } from './claude/claude-strategy.js';
import { ResponsesAPIStrategy } from './openai/openai-responses-strategy.js';
/**
* Strategy factory that returns the appropriate strategy instance based on the provider protocol.
@ -13,6 +14,8 @@ class ProviderStrategyFactory {
return new GeminiStrategy();
case MODEL_PROTOCOL_PREFIX.OPENAI:
return new OpenAIStrategy();
case MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES:
return new ResponsesAPIStrategy();
case MODEL_PROTOCOL_PREFIX.CLAUDE:
return new ClaudeStrategy();
default: