mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +00:00
fix: 调整gemini-api BaseApi后缀以适配更多端点
This commit is contained in:
@@ -22,6 +22,54 @@ const ProxyHelper = require('../utils/proxyHelper')
|
|||||||
// 工具函数
|
// 工具函数
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建 Gemini API URL
|
||||||
|
* 兼容新旧 baseUrl 格式:
|
||||||
|
* - 新格式(以 /models 结尾): https://xxx.com/v1beta/models -> 直接拼接 /{model}:action
|
||||||
|
* - 旧格式(不以 /models 结尾): https://xxx.com -> 拼接 /v1beta/models/{model}:action
|
||||||
|
*
|
||||||
|
* @param {string} baseUrl - 账户配置的基础地址
|
||||||
|
* @param {string} model - 模型名称
|
||||||
|
* @param {string} action - API 动作 (generateContent, streamGenerateContent, countTokens)
|
||||||
|
* @param {string} apiKey - API Key
|
||||||
|
* @param {object} options - 额外选项 { stream: boolean, listModels: boolean }
|
||||||
|
* @returns {string} 完整的 API URL
|
||||||
|
*/
|
||||||
|
function buildGeminiApiUrl(baseUrl, model, action, apiKey, options = {}) {
|
||||||
|
const { stream = false, listModels = false } = options
|
||||||
|
|
||||||
|
// 移除末尾的斜杠(如果有)
|
||||||
|
const normalizedBaseUrl = baseUrl.replace(/\/+$/, '')
|
||||||
|
|
||||||
|
// 检查是否为新格式(以 /models 结尾)
|
||||||
|
const isNewFormat = normalizedBaseUrl.endsWith('/models')
|
||||||
|
|
||||||
|
let url
|
||||||
|
if (listModels) {
|
||||||
|
// 获取模型列表
|
||||||
|
if (isNewFormat) {
|
||||||
|
// 新格式: baseUrl 已包含 /v1beta/models,直接添加查询参数
|
||||||
|
url = `${normalizedBaseUrl}?key=${apiKey}`
|
||||||
|
} else {
|
||||||
|
// 旧格式: 需要拼接 /v1beta/models
|
||||||
|
url = `${normalizedBaseUrl}/v1beta/models?key=${apiKey}`
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 模型操作 (generateContent, streamGenerateContent, countTokens)
|
||||||
|
const streamParam = stream ? '&alt=sse' : ''
|
||||||
|
|
||||||
|
if (isNewFormat) {
|
||||||
|
// 新格式: baseUrl 已包含 /v1beta/models,直接拼接 /{model}:action
|
||||||
|
url = `${normalizedBaseUrl}/${model}:${action}?key=${apiKey}${streamParam}`
|
||||||
|
} else {
|
||||||
|
// 旧格式: 需要拼接 /v1beta/models/{model}:action
|
||||||
|
url = `${normalizedBaseUrl}/v1beta/models/${model}:${action}?key=${apiKey}${streamParam}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 生成会话哈希
|
* 生成会话哈希
|
||||||
*/
|
*/
|
||||||
@@ -378,9 +426,13 @@ async function handleMessages(req, res) {
|
|||||||
// 解析代理配置
|
// 解析代理配置
|
||||||
const proxyConfig = parseProxyConfig(account)
|
const proxyConfig = parseProxyConfig(account)
|
||||||
|
|
||||||
const apiUrl = stream
|
const apiUrl = buildGeminiApiUrl(
|
||||||
? `${account.baseUrl}/v1beta/models/${model}:streamGenerateContent?key=${account.apiKey}&alt=sse`
|
account.baseUrl,
|
||||||
: `${account.baseUrl}/v1beta/models/${model}:generateContent?key=${account.apiKey}`
|
model,
|
||||||
|
stream ? 'streamGenerateContent' : 'generateContent',
|
||||||
|
account.apiKey,
|
||||||
|
{ stream }
|
||||||
|
)
|
||||||
|
|
||||||
const axiosConfig = {
|
const axiosConfig = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -671,7 +723,9 @@ async function handleModels(req, res) {
|
|||||||
// API Key 账户:使用 API Key 获取模型列表
|
// API Key 账户:使用 API Key 获取模型列表
|
||||||
const proxyConfig = parseProxyConfig(account)
|
const proxyConfig = parseProxyConfig(account)
|
||||||
try {
|
try {
|
||||||
const apiUrl = `${account.baseUrl}/v1beta/models?key=${account.apiKey}`
|
const apiUrl = buildGeminiApiUrl(account.baseUrl, null, null, account.apiKey, {
|
||||||
|
listModels: true
|
||||||
|
})
|
||||||
const axiosConfig = {
|
const axiosConfig = {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: apiUrl,
|
url: apiUrl,
|
||||||
@@ -1169,8 +1223,8 @@ async function handleCountTokens(req, res) {
|
|||||||
let response
|
let response
|
||||||
if (isApiAccount) {
|
if (isApiAccount) {
|
||||||
// API Key 账户:直接使用 API Key 请求
|
// API Key 账户:直接使用 API Key 请求
|
||||||
const modelPath = model.startsWith('models/') ? model : `models/${model}`
|
const modelName = model.startsWith('models/') ? model.replace('models/', '') : model
|
||||||
const apiUrl = `${account.baseUrl}/v1beta/${modelPath}:countTokens?key=${account.apiKey}`
|
const apiUrl = buildGeminiApiUrl(account.baseUrl, modelName, 'countTokens', account.apiKey)
|
||||||
|
|
||||||
const axiosConfig = {
|
const axiosConfig = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -1897,7 +1951,7 @@ async function handleStandardGenerateContent(req, res) {
|
|||||||
|
|
||||||
if (isApiAccount) {
|
if (isApiAccount) {
|
||||||
// Gemini API 账户:直接使用 API Key 请求
|
// Gemini API 账户:直接使用 API Key 请求
|
||||||
const apiUrl = `${account.baseUrl}/v1beta/models/${model}:generateContent?key=${account.apiKey}`
|
const apiUrl = buildGeminiApiUrl(account.baseUrl, model, 'generateContent', account.apiKey)
|
||||||
|
|
||||||
const axiosConfig = {
|
const axiosConfig = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -2168,7 +2222,15 @@ async function handleStandardStreamGenerateContent(req, res) {
|
|||||||
|
|
||||||
if (isApiAccount) {
|
if (isApiAccount) {
|
||||||
// Gemini API 账户:直接使用 API Key 请求流式接口
|
// Gemini API 账户:直接使用 API Key 请求流式接口
|
||||||
const apiUrl = `${account.baseUrl}/v1beta/models/${model}:streamGenerateContent?key=${account.apiKey}&alt=sse`
|
const apiUrl = buildGeminiApiUrl(
|
||||||
|
account.baseUrl,
|
||||||
|
model,
|
||||||
|
'streamGenerateContent',
|
||||||
|
account.apiKey,
|
||||||
|
{
|
||||||
|
stream: true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
const axiosConfig = {
|
const axiosConfig = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|||||||
@@ -1524,24 +1524,32 @@
|
|||||||
<input
|
<input
|
||||||
v-model="form.baseUrl"
|
v-model="form.baseUrl"
|
||||||
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
|
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
|
||||||
placeholder="https://generativelanguage.googleapis.com"
|
:class="{ 'border-red-500 dark:border-red-400': errors.baseUrl }"
|
||||||
|
placeholder="https://generativelanguage.googleapis.com/v1beta/models"
|
||||||
required
|
required
|
||||||
type="url"
|
type="url"
|
||||||
/>
|
/>
|
||||||
|
<p v-if="errors.baseUrl" class="mt-1 text-xs text-red-500 dark:text-red-400">
|
||||||
|
{{ errors.baseUrl }}
|
||||||
|
</p>
|
||||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||||
填写 API 基础地址(可包含路径前缀),系统会自动拼接
|
填写 API 基础地址,必须以
|
||||||
|
<code class="rounded bg-gray-100 px-1 dark:bg-gray-600">/models</code>
|
||||||
|
结尾。系统会自动拼接
|
||||||
<code class="rounded bg-gray-100 px-1 dark:bg-gray-600"
|
<code class="rounded bg-gray-100 px-1 dark:bg-gray-600"
|
||||||
>/v1beta/models/{model}:generateContent</code
|
>/{model}:generateContent</code
|
||||||
>
|
>
|
||||||
</p>
|
</p>
|
||||||
<p class="mt-0.5 text-xs text-gray-400 dark:text-gray-500">
|
<p class="mt-0.5 text-xs text-gray-400 dark:text-gray-500">
|
||||||
官方:
|
官方:
|
||||||
<code class="rounded bg-gray-100 px-1 dark:bg-gray-600"
|
<code class="rounded bg-gray-100 px-1 dark:bg-gray-600"
|
||||||
>https://generativelanguage.googleapis.com</code
|
>https://generativelanguage.googleapis.com/v1beta/models</code
|
||||||
>
|
>
|
||||||
| 上游为 CRS:
|
</p>
|
||||||
|
<p class="mt-0.5 text-xs text-gray-400 dark:text-gray-500">
|
||||||
|
上游为 CRS:
|
||||||
<code class="rounded bg-gray-100 px-1 dark:bg-gray-600"
|
<code class="rounded bg-gray-100 px-1 dark:bg-gray-600"
|
||||||
>https://your-crs.com/gemini</code
|
>https://your-crs.com/gemini/v1beta/models</code
|
||||||
>
|
>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -3025,23 +3033,31 @@
|
|||||||
<input
|
<input
|
||||||
v-model="form.baseUrl"
|
v-model="form.baseUrl"
|
||||||
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200"
|
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200"
|
||||||
placeholder="https://generativelanguage.googleapis.com"
|
:class="{ 'border-red-500 dark:border-red-400': errors.baseUrl }"
|
||||||
|
placeholder="https://generativelanguage.googleapis.com/v1beta/models"
|
||||||
type="url"
|
type="url"
|
||||||
/>
|
/>
|
||||||
|
<p v-if="errors.baseUrl" class="mt-1 text-xs text-red-500 dark:text-red-400">
|
||||||
|
{{ errors.baseUrl }}
|
||||||
|
</p>
|
||||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||||
填写 API 基础地址(可包含路径前缀),系统会自动拼接
|
填写 API 基础地址,必须以
|
||||||
|
<code class="rounded bg-gray-100 px-1 dark:bg-gray-600">/models</code>
|
||||||
|
结尾。系统会自动拼接
|
||||||
<code class="rounded bg-gray-100 px-1 dark:bg-gray-600"
|
<code class="rounded bg-gray-100 px-1 dark:bg-gray-600"
|
||||||
>/v1beta/models/{model}:generateContent</code
|
>/{model}:generateContent</code
|
||||||
>
|
>
|
||||||
</p>
|
</p>
|
||||||
<p class="mt-0.5 text-xs text-gray-400 dark:text-gray-500">
|
<p class="mt-0.5 text-xs text-gray-400 dark:text-gray-500">
|
||||||
官方:
|
官方:
|
||||||
<code class="rounded bg-gray-100 px-1 dark:bg-gray-600"
|
<code class="rounded bg-gray-100 px-1 dark:bg-gray-600"
|
||||||
>https://generativelanguage.googleapis.com</code
|
>https://generativelanguage.googleapis.com/v1beta/models</code
|
||||||
>
|
>
|
||||||
| 上游为 CRS:
|
</p>
|
||||||
|
<p class="mt-0.5 text-xs text-gray-400 dark:text-gray-500">
|
||||||
|
上游为 CRS:
|
||||||
<code class="rounded bg-gray-100 px-1 dark:bg-gray-600"
|
<code class="rounded bg-gray-100 px-1 dark:bg-gray-600"
|
||||||
>https://your-crs.com/gemini</code
|
>https://your-crs.com/gemini/v1beta/models</code
|
||||||
>
|
>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -4485,6 +4501,14 @@ const createAccount = async () => {
|
|||||||
errors.value.apiKey = '请填写 API Key'
|
errors.value.apiKey = '请填写 API Key'
|
||||||
hasError = true
|
hasError = true
|
||||||
}
|
}
|
||||||
|
// 验证 baseUrl 必须以 /models 结尾
|
||||||
|
if (!form.value.baseUrl || form.value.baseUrl.trim() === '') {
|
||||||
|
errors.value.baseUrl = '请填写 API 基础地址'
|
||||||
|
hasError = true
|
||||||
|
} else if (!form.value.baseUrl.trim().endsWith('/models')) {
|
||||||
|
errors.value.baseUrl = 'API 基础地址必须以 /models 结尾'
|
||||||
|
hasError = true
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// 其他平台(如 Droid)使用多 API Key 输入
|
// 其他平台(如 Droid)使用多 API Key 输入
|
||||||
const apiKeys = parseApiKeysInput(form.value.apiKeysInput)
|
const apiKeys = parseApiKeysInput(form.value.apiKeysInput)
|
||||||
@@ -4748,6 +4772,7 @@ const updateAccount = async () => {
|
|||||||
// 清除之前的错误
|
// 清除之前的错误
|
||||||
errors.value.name = ''
|
errors.value.name = ''
|
||||||
errors.value.apiKeys = ''
|
errors.value.apiKeys = ''
|
||||||
|
errors.value.baseUrl = ''
|
||||||
|
|
||||||
// 验证账户名称
|
// 验证账户名称
|
||||||
if (!form.value.name || form.value.name.trim() === '') {
|
if (!form.value.name || form.value.name.trim() === '') {
|
||||||
@@ -4755,6 +4780,19 @@ const updateAccount = async () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Gemini API 的 baseUrl 验证(必须以 /models 结尾)
|
||||||
|
if (form.value.platform === 'gemini-api') {
|
||||||
|
const baseUrl = form.value.baseUrl?.trim() || ''
|
||||||
|
if (!baseUrl) {
|
||||||
|
errors.value.baseUrl = '请填写 API 基础地址'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!baseUrl.endsWith('/models')) {
|
||||||
|
errors.value.baseUrl = 'API 基础地址必须以 /models 结尾'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 分组类型验证 - 更新账户流程修复
|
// 分组类型验证 - 更新账户流程修复
|
||||||
if (
|
if (
|
||||||
form.value.accountType === 'group' &&
|
form.value.accountType === 'group' &&
|
||||||
|
|||||||
@@ -229,7 +229,7 @@
|
|||||||
<i v-else class="fas fa-sort ml-1 text-gray-400" />
|
<i v-else class="fas fa-sort ml-1 text-gray-400" />
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th
|
||||||
class="w-[100px] min-w-[120px] max-w-[150px] cursor-pointer px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600"
|
class="w-[120px] min-w-[180px] max-w-[20s0px] cursor-pointer px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600"
|
||||||
@click="sortAccounts('status')"
|
@click="sortAccounts('status')"
|
||||||
>
|
>
|
||||||
状态
|
状态
|
||||||
|
|||||||
Reference in New Issue
Block a user