mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-04-19 12:58:39 +00:00
Merge pull request #1041 from X-Zero-L/feat/codex-api-configurable-endpoint [skip ci]
feat: configurable provider endpoint for codex-api accounts
This commit is contained in:
@@ -472,9 +472,18 @@ router.post('/openai-responses-accounts/:accountId/test', authenticateAdmin, asy
|
|||||||
return res.status(401).json({ error: 'API Key not found or decryption failed' })
|
return res.status(401).json({ error: 'API Key not found or decryption failed' })
|
||||||
}
|
}
|
||||||
|
|
||||||
// 构造测试请求
|
// 构造测试请求(根据 providerEndpoint 和 baseApi 决定端点路径)
|
||||||
const baseUrl = account.baseApi || 'https://api.openai.com'
|
const baseUrl = account.baseApi || 'https://api.openai.com'
|
||||||
const apiUrl = `${baseUrl}/responses`
|
const providerEndpoint = account.providerEndpoint || 'responses'
|
||||||
|
let endpointPath = '/responses'
|
||||||
|
if (providerEndpoint === 'auto') {
|
||||||
|
endpointPath = '/responses' // 测试时默认用 responses
|
||||||
|
}
|
||||||
|
// 防止 baseApi 已含 /v1 时路径重复
|
||||||
|
if (!baseUrl.endsWith('/v1')) {
|
||||||
|
endpointPath = `/v1${endpointPath}`
|
||||||
|
}
|
||||||
|
const apiUrl = `${baseUrl}${endpointPath}`
|
||||||
const payload = createOpenAITestPayload(model, { stream: false })
|
const payload = createOpenAITestPayload(model, { stream: false })
|
||||||
|
|
||||||
const requestConfig = {
|
const requestConfig = {
|
||||||
|
|||||||
@@ -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 | auto
|
||||||
} = options
|
} = options
|
||||||
|
|
||||||
// 验证必填字段
|
// 验证必填字段
|
||||||
@@ -59,6 +60,14 @@ class OpenAIResponsesAccountService {
|
|||||||
throw new Error('Base API URL and API Key are required for OpenAI-Responses account')
|
throw new Error('Base API URL and API Key are required for OpenAI-Responses account')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 验证 providerEndpoint 枚举值
|
||||||
|
const validEndpoints = ['responses', 'auto']
|
||||||
|
if (!validEndpoints.includes(providerEndpoint)) {
|
||||||
|
throw new Error(
|
||||||
|
`Invalid providerEndpoint: ${providerEndpoint}. Must be one of: ${validEndpoints.join(', ')}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// 规范化 baseApi(确保不以 / 结尾)
|
// 规范化 baseApi(确保不以 / 结尾)
|
||||||
const normalizedBaseApi = baseApi.endsWith('/') ? baseApi.slice(0, -1) : baseApi
|
const normalizedBaseApi = baseApi.endsWith('/') ? baseApi.slice(0, -1) : baseApi
|
||||||
|
|
||||||
@@ -96,7 +105,8 @@ class OpenAIResponsesAccountService {
|
|||||||
lastResetDate: redis.getDateStringInTimezone(),
|
lastResetDate: redis.getDateStringInTimezone(),
|
||||||
quotaResetTime,
|
quotaResetTime,
|
||||||
quotaStoppedAt: '',
|
quotaStoppedAt: '',
|
||||||
disableAutoProtection: disableAutoProtection.toString() // 关闭自动防护
|
disableAutoProtection: disableAutoProtection.toString(), // 关闭自动防护
|
||||||
|
providerEndpoint // Provider 端点类型:responses(默认) | auto
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存到 Redis
|
// 保存到 Redis
|
||||||
@@ -165,6 +175,16 @@ class OpenAIResponsesAccountService {
|
|||||||
// 直接保存,不做任何调整
|
// 直接保存,不做任何调整
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 验证 providerEndpoint 枚举值
|
||||||
|
if (updates.providerEndpoint !== undefined) {
|
||||||
|
const validEndpoints = ['responses', 'auto']
|
||||||
|
if (!validEndpoints.includes(updates.providerEndpoint)) {
|
||||||
|
throw new Error(
|
||||||
|
`Invalid providerEndpoint: ${updates.providerEndpoint}. Must be one of: ${validEndpoints.join(', ')}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 自动防护开关
|
// 自动防护开关
|
||||||
if (updates.disableAutoProtection !== undefined) {
|
if (updates.disableAutoProtection !== undefined) {
|
||||||
updates.disableAutoProtection = updates.disableAutoProtection.toString()
|
updates.disableAutoProtection = updates.disableAutoProtection.toString()
|
||||||
|
|||||||
@@ -91,8 +91,30 @@ 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
|
||||||
|
|
||||||
|
// 根据 providerEndpoint 配置归一化路径
|
||||||
|
// 注意:unified.js 已将 /v1/chat/completions 的请求体转换为 Responses 格式,
|
||||||
|
// 因此这里只需归一化路径即可;反向 responses→completions 需要同时转换请求体,
|
||||||
|
// 目前不支持,所以只保留 responses 和 auto 两种模式
|
||||||
|
if (
|
||||||
|
providerEndpoint === 'responses' &&
|
||||||
|
(targetPath === '/v1/chat/completions' || targetPath === '/chat/completions')
|
||||||
|
) {
|
||||||
|
const newPath = targetPath.startsWith('/v1') ? '/v1/responses' : '/responses'
|
||||||
|
logger.info(`📝 Normalized path (${req.path}) → ${newPath} (providerEndpoint=responses)`)
|
||||||
|
targetPath = newPath
|
||||||
|
}
|
||||||
|
// providerEndpoint === 'auto' 时保持原始路径不变
|
||||||
|
|
||||||
|
// 防止 baseApi 已含 /v1 时路径重复(如 baseApi=http://host/v1 + targetPath=/v1/responses → /v1/v1/responses)
|
||||||
|
const baseApi = fullAccount.baseApi || ''
|
||||||
|
if (baseApi.endsWith('/v1') && targetPath.startsWith('/v1/')) {
|
||||||
|
targetPath = targetPath.slice(3) // '/v1/responses' → '/responses'
|
||||||
|
}
|
||||||
|
const targetUrl = `${baseApi}${targetPath}`
|
||||||
logger.info(`🎯 Forwarding to: ${targetUrl}`)
|
logger.info(`🎯 Forwarding to: ${targetUrl}`)
|
||||||
|
|
||||||
// 构建请求头 - 使用统一的 headerFilter 移除 CDN headers
|
// 构建请求头 - 使用统一的 headerFilter 移除 CDN headers
|
||||||
|
|||||||
@@ -1692,6 +1692,23 @@
|
|||||||
</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="auto">自动(保持原始路径)</option>
|
||||||
|
</select>
|
||||||
|
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
指定 Provider 支持的端点类型。Responses 会将所有请求路由到(包括来自
|
||||||
|
/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 +3457,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/chat/completions 的请求会自动转换);自动则保持原始路径
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 限流时长字段 - 隐藏不显示,保持原值 -->
|
<!-- 限流时长字段 - 隐藏不显示,保持原值 -->
|
||||||
<input v-model.number="form.rateLimitDuration" type="hidden" />
|
<input v-model.number="form.rateLimitDuration" type="hidden" />
|
||||||
|
|
||||||
@@ -4272,6 +4307,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 +5470,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 +5821,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 +6444,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',
|
||||||
// 额度管理字段
|
// 额度管理字段
|
||||||
|
|||||||
Reference in New Issue
Block a user