feat: add configurable provider endpoint for codex-api accounts

Add providerEndpoint field (responses/completions/auto) to openai-responses
accounts, allowing admins to specify which API endpoint format the provider
supports. The relay service normalizes request paths based on this config:
- responses (default): routes all requests to /v1/responses
- completions: routes all requests to /v1/chat/completions
- auto: preserves the original request path

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
X-Zero-L
2026-03-01 19:48:17 +08:00
parent 0871101f27
commit 0831739f4b
3 changed files with 60 additions and 4 deletions

View File

@@ -51,7 +51,8 @@ class OpenAIResponsesAccountService {
dailyQuota = 0, // 每日额度限制美元0表示不限制 dailyQuota = 0, // 每日额度限制美元0表示不限制
quotaResetTime = '00:00', // 额度重置时间HH:mm格式 quotaResetTime = '00:00', // 额度重置时间HH:mm格式
rateLimitDuration = 60, // 限流时间(分钟) rateLimitDuration = 60, // 限流时间(分钟)
disableAutoProtection = false // 是否关闭自动防护429/401/400/529 不自动禁用) disableAutoProtection = false, // 是否关闭自动防护429/401/400/529 不自动禁用)
providerEndpoint = 'responses' // Provider 端点类型responses | completions | auto
} = options } = options
// 验证必填字段 // 验证必填字段
@@ -96,7 +97,8 @@ class OpenAIResponsesAccountService {
lastResetDate: redis.getDateStringInTimezone(), lastResetDate: redis.getDateStringInTimezone(),
quotaResetTime, quotaResetTime,
quotaStoppedAt: '', quotaStoppedAt: '',
disableAutoProtection: disableAutoProtection.toString() // 关闭自动防护 disableAutoProtection: disableAutoProtection.toString(), // 关闭自动防护
providerEndpoint // Provider 端点类型responses(默认) | completions | auto
} }
// 保存到 Redis // 保存到 Redis

View File

@@ -91,8 +91,22 @@ class OpenAIResponsesRelayService {
req.once('close', handleClientDisconnect) req.once('close', handleClientDisconnect)
res.once('close', handleClientDisconnect) res.once('close', handleClientDisconnect)
// 构建目标 URL // 构建目标 URL(根据 providerEndpoint 配置决定端点路径)
const targetUrl = `${fullAccount.baseApi}${req.path}` const providerEndpoint = fullAccount.providerEndpoint || 'responses'
let targetPath = req.path
if (providerEndpoint === 'responses' && targetPath.includes('/completions')) {
// Provider 仅支持 Responses 端点,将 completions 路径归一化
targetPath = '/v1/responses'
logger.info(`📝 Normalized path (${req.path}) → /v1/responses (providerEndpoint=responses)`)
} else if (providerEndpoint === 'completions' && targetPath.includes('/responses')) {
// Provider 仅支持 Completions 端点,将 responses 路径归一化
targetPath = '/v1/chat/completions'
logger.info(
`📝 Normalized path (${req.path}) → /v1/chat/completions (providerEndpoint=completions)`
)
}
// providerEndpoint === 'auto' 时保持原始路径不变
const targetUrl = `${fullAccount.baseApi}${targetPath}`
logger.info(`🎯 Forwarding to: ${targetUrl}`) logger.info(`🎯 Forwarding to: ${targetUrl}`)
// 构建请求头 - 使用统一的 headerFilter 移除 CDN headers // 构建请求头 - 使用统一的 headerFilter 移除 CDN headers

View File

@@ -1692,6 +1692,24 @@
</p> </p>
</div> </div>
<div>
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300"
>Provider 端点类型</label
>
<select
v-model="form.providerEndpoint"
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200"
>
<option value="responses">Responses推荐</option>
<option value="completions">Chat Completions</option>
<option value="auto">自动(保持原始路径)</option>
</select>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
指定 Provider 支持的端点类型。Responses 会将所有请求路由到 /v1/responsesChat
Completions 路由到 /v1/chat/completions自动则保持客户端请求的原始路径
</p>
</div>
<!-- 限流时长字段 - 隐藏不显示使用默认值60 --> <!-- 限流时长字段 - 隐藏不显示使用默认值60 -->
<input v-model.number="form.rateLimitDuration" type="hidden" value="60" /> <input v-model.number="form.rateLimitDuration" type="hidden" value="60" />
</div> </div>
@@ -3440,6 +3458,24 @@
</p> </p>
</div> </div>
<div>
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300"
>Provider 端点类型</label
>
<select
v-model="form.providerEndpoint"
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200"
>
<option value="responses">Responses推荐</option>
<option value="completions">Chat Completions</option>
<option value="auto">自动(保持原始路径)</option>
</select>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
指定 Provider 支持的端点类型。Responses 路由到 /v1/responsesChat Completions
路由到 /v1/chat/completions自动则保持原始路径
</p>
</div>
<!-- 限流时长字段 - 隐藏不显示,保持原值 --> <!-- 限流时长字段 - 隐藏不显示,保持原值 -->
<input v-model.number="form.rateLimitDuration" type="hidden" /> <input v-model.number="form.rateLimitDuration" type="hidden" />
@@ -4272,6 +4308,7 @@ const form = ref({
endpointType: props.account?.endpointType || 'anthropic', endpointType: props.account?.endpointType || 'anthropic',
// OpenAI-Responses 特定字段 // OpenAI-Responses 特定字段
baseApi: props.account?.baseApi || '', baseApi: props.account?.baseApi || '',
providerEndpoint: props.account?.providerEndpoint || 'responses',
// Gemini-API 特定字段 // Gemini-API 特定字段
baseUrl: props.account?.baseUrl || 'https://generativelanguage.googleapis.com', baseUrl: props.account?.baseUrl || 'https://generativelanguage.googleapis.com',
rateLimitDuration: props.account?.rateLimitDuration || 60, rateLimitDuration: props.account?.rateLimitDuration || 60,
@@ -5434,6 +5471,7 @@ const createAccount = async () => {
data.baseApi = form.value.baseApi data.baseApi = form.value.baseApi
data.apiKey = form.value.apiKey data.apiKey = form.value.apiKey
data.userAgent = form.value.userAgent || '' data.userAgent = form.value.userAgent || ''
data.providerEndpoint = form.value.providerEndpoint || 'responses'
data.priority = form.value.priority || 50 data.priority = form.value.priority || 50
data.rateLimitDuration = 60 // 默认值60不从用户输入获取 data.rateLimitDuration = 60 // 默认值60不从用户输入获取
data.dailyQuota = form.value.dailyQuota || 0 data.dailyQuota = form.value.dailyQuota || 0
@@ -5784,6 +5822,7 @@ const updateAccount = async () => {
data.apiKey = form.value.apiKey data.apiKey = form.value.apiKey
} }
data.userAgent = form.value.userAgent || '' data.userAgent = form.value.userAgent || ''
data.providerEndpoint = form.value.providerEndpoint || 'responses'
data.priority = form.value.priority || 50 data.priority = form.value.priority || 50
// 编辑时不上传 rateLimitDuration保持原值 // 编辑时不上传 rateLimitDuration保持原值
data.dailyQuota = form.value.dailyQuota || 0 data.dailyQuota = form.value.dailyQuota || 0
@@ -6406,6 +6445,7 @@ watch(
deploymentName: newAccount.deploymentName || '', deploymentName: newAccount.deploymentName || '',
// OpenAI-Responses 特定字段 // OpenAI-Responses 特定字段
baseApi: newAccount.baseApi || '', baseApi: newAccount.baseApi || '',
providerEndpoint: newAccount.providerEndpoint || 'responses',
// Gemini-API 特定字段 // Gemini-API 特定字段
baseUrl: newAccount.baseUrl || 'https://generativelanguage.googleapis.com', baseUrl: newAccount.baseUrl || 'https://generativelanguage.googleapis.com',
// 额度管理字段 // 额度管理字段