feat(api): 添加 OpenAI Responses API 支持
新增对 OpenAI Responses API 端点的部分支持,包括请求转换、流式响应处理和供应商适配。主要变更: - 新增 OpenAIResponsesApiService 核心服务实现 - 实现 Claude/Gemini 到 Responses API 的双向协议转换(能聊天,不能调用工具) - 添加流式响应状态管理和事件生成机制 - 扩展路由支持 /v1/responses 端点 - 更新文档说明配置方法和使用示例
This commit is contained in:
parent
d36a9466a8
commit
dfe7ce914e
11 changed files with 1332 additions and 55 deletions
15
README.md
15
README.md
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
}
|
||||
}
|
||||
|
|
@ -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`);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
654
src/convert.js
654
src/convert.js
|
|
@ -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)
|
||||
];
|
||||
}
|
||||
146
src/openai/openai-responses-core.js
Normal file
146
src/openai/openai-responses-core.js
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
329
src/openai/openai-responses-core.mjs
Normal file
329
src/openai/openai-responses-core.mjs
Normal 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 };
|
||||
123
src/openai/openai-responses-strategy.js
Normal file
123
src/openai/openai-responses-strategy.js
Normal 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 };
|
||||
|
|
@ -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?' }];
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Reference in a new issue