feat(channels): 添加对 Gemini 渠道的支持和相关配置

This commit is contained in:
Jeff Gu committed 2026-04-10 18:00:21 +08:00
1 parent 88ae321d4f
commit 7707de2b3e
10 files changed
+108 -18

No files matched your search

+20 -3
View File
@@ -10,7 +10,7 @@
## ✨ 功能特性
- 🚀 **基于 Cloudflare Workers**:全球分布式网络、低延迟、高性能
- 🔐 **多渠道**:支持 OpenAI、Azure OpenAI、Claude 等多种 AI 服务商
- 🔐 **多渠道**:支持 OpenAI、Gemini、Azure OpenAI、Claude 等多种 AI 服务商
- 🎫 **负载均衡**:支持多KEY/渠道间分配请求,自动重试+自动轮换,确保高可用性
- 📊 **用量统计<sup>*</sup>**:Analytics Engine 实时统计,精确成本核算与日志回溯
- 💰 **定价管理**:灵活的模型定价配置
@@ -57,7 +57,7 @@ one-api-workers/
│ │ └── index.ts # 管理接口路由
│ ├── providers/ # AI 服务商
│ │ ├── azure-openai-proxy.ts # Azure OpenAI 代理
│ │ ├── openai-proxy.ts # OpenAI 代理
│ │ ├── openai-proxy.ts # OpenAI / Gemini 兼容代理
│ │ ├── claude-proxy.ts # Claude 代理
│ │ ├── openai-responses-proxy.ts # OpenAI Responses 代理
│ │ ├── azure-openai-responses-proxy.ts # Azure Responses 代理
@@ -286,6 +286,23 @@ curl https://your-domain.com/v1/responses \
}
```
**Gemini 配置**
Gemini 通过官方 OpenAI 兼容层接入,建议将 `endpoint` 配置为 `https://generativelanguage.googleapis.com/v1beta/openai/`。
```json
{
"name": "My Gemini Channel",
"type": "gemini",
"endpoint": "https://generativelanguage.googleapis.com/v1beta/openai/",
"api_key": "AIza-your-gemini-api-key",
"deployment_mapper": {
"gemini-2.5-flash": "gemini-2.5-flash",
"gemini-2.5-pro": "gemini-2.5-pro"
}
}
```
**Azure OpenAI 配置**
```json
@@ -348,7 +365,7 @@ curl https://your-domain.com/v1/responses \
**配置字段说明**:
- `name`: 渠道显示名称
- `type`: 服务商类型(`openai`、`azure-openai`、`claude`、`openai-responses`、`azure-openai-responses`)
- `type`: 服务商类型(`openai`、`gemini`、`azure-openai`、`claude`、`openai-responses`、`azure-openai-responses`)
- `endpoint`: API 端点地址
- `api_key`: API 密钥
- `api_version`: API 版本(Azure OpenAI / Claude 可用;Azure Responses v1 请留空)
+47 -6
View File
@@ -35,12 +35,13 @@ type ModelRow = { id: string; name: string }
const channelTypes = [
{ value: 'openai', label: 'OpenAI' },
{ value: 'openai-responses', label: 'OpenAI Responses' },
{ value: 'claude', label: 'Claude' },
{ value: 'gemini', label: 'Gemini' },
{ value: 'openai-audio', label: 'OpenAI Audio' },
{ value: 'claude-to-openai', label: 'Claude → OpenAI' },
{ value: 'azure-openai', label: 'Azure OpenAI' },
{ value: 'azure-openai-audio', label: 'Azure OpenAI Audio' },
{ value: 'openai-audio', label: 'OpenAI Audio' },
{ value: 'claude', label: 'Claude' },
{ value: 'claude-to-openai', label: 'Claude → OpenAI' },
{ value: 'openai-responses', label: 'OpenAI Responses' },
{ value: 'azure-openai-responses', label: 'Azure OpenAI Responses' },
]
@@ -283,6 +284,10 @@ const getChannelRequestPreviewPath = (type: string | undefined): string => {
}
}
const isGeminiChannelType = (type: string | undefined): boolean => {
return type === 'gemini'
}
const isOpenAIStyleChannelType = (type: string | undefined): boolean => {
return ['openai', 'openai-audio', 'openai-responses', 'claude-to-openai'].includes(type || '')
}
@@ -335,11 +340,42 @@ const buildClaudeEndpointPreview = (endpoint: string, requestPath: string): stri
return buildPrefixedEndpointPreview(endpoint, requestPath)
}
const buildGeminiEndpointPreview = (endpoint: string, requestPath: string): string => {
const targetUrl = new URL(endpoint)
const currentBasePath = trimSlashes(targetUrl.pathname)
const normalizedRequestPath = requestPath.replace(/^\/v1(?=\/|$)/, '')
const geminiBasePath = currentBasePath.endsWith('v1beta/openai')
? currentBasePath
: currentBasePath.endsWith('v1beta')
? joinPath(currentBasePath, 'openai')
: currentBasePath.endsWith('openai')
? currentBasePath
: joinPath(currentBasePath, 'v1beta/openai')
targetUrl.pathname = joinPath(geminiBasePath, normalizedRequestPath)
return targetUrl.toString()
}
const buildFallbackEndpointPreview = (endpoint: string, requestPath: string): string => {
const normalizedEndpoint = endpoint.trim().replace(/\/+$/, '')
return `${normalizedEndpoint}${requestPath}`
}
const getChannelEndpointPlaceholder = (type: string | undefined): string => {
switch (type) {
case 'gemini':
return 'https://generativelanguage.googleapis.com/v1beta/openai/'
case 'azure-openai':
case 'azure-openai-audio':
case 'azure-openai-responses':
return 'https://your-resource.openai.azure.com/'
case 'claude':
return 'https://api.anthropic.com/v1/'
default:
return 'https://api.openai.com/v1/'
}
}
const getChannelEndpointPreview = (type: string | undefined, endpoint: string): string => {
const trimmedEndpoint = endpoint.trim()
const requestPath = getChannelRequestPreviewPath(type)
@@ -352,6 +388,10 @@ const getChannelEndpointPreview = (type: string | undefined, endpoint: string):
return buildClaudeEndpointPreview(trimmedEndpoint, requestPath)
}
if (isGeminiChannelType(type)) {
return buildGeminiEndpointPreview(trimmedEndpoint, requestPath)
}
if (isAzureChannelType(type)) {
return buildAzureEndpointPreview(trimmedEndpoint, requestPath)
}
@@ -725,6 +765,7 @@ export function Channels({
)
})
const endpointPreview = getChannelEndpointPreview(formData.type, formData.endpoint)
const endpointPlaceholder = getChannelEndpointPlaceholder(formData.type)
if (view === 'list') {
return (
@@ -772,7 +813,7 @@ export function Channels({
</div>
<h3 className="font-semibold text-lg mb-2">添加您的第一个渠道</h3>
<p className="text-muted-foreground text-sm text-center max-w-sm mb-6">
连接到 AI 服务商(如 OpenAI、Azure、Claude),用于代理和转发 API 请求。
连接到 AI 服务商(如 OpenAI、Gemini、Azure、Claude),用于代理和转发 API 请求。
</p>
<Button onClick={handleAdd} size="lg">
<Plus className="h-4 w-4 mr-2" />
@@ -963,7 +1004,7 @@ export function Channels({
<Input
value={formData.endpoint}
onChange={(e) => setFormData({ ...formData, endpoint: e.target.value })}
placeholder="https://your-resource.openai.azure.com/"
placeholder={endpointPlaceholder}
/>
<p className="text-xs text-muted-foreground">
预览:{endpointPreview || formData.type}
+2 -2
View File
@@ -8,7 +8,7 @@ const features = [
{
icon: Globe,
title: "多渠道支持",
description: "支持 OpenAI、Azure OpenAI、Claude 等多种 AI 服务商。",
description: "支持 OpenAI、Claude、Gemini、Azure OpenAI 等多种 AI 服务商。",
gradient: "from-blue-500/10 to-cyan-500/10",
iconBg: "from-blue-500 to-cyan-500",
delay: "0ms",
@@ -149,7 +149,7 @@ export function Dashboard() {
<div className="text-center py-4">
<p className="text-xs text-muted-foreground/60 uppercase tracking-wider mb-4">支持的服务端点</p>
<div className="flex items-center justify-center gap-6 flex-wrap text-muted-foreground/40">
{["OpenAI", "Azure OpenAI", "Claude", "OpenAI Responses"].map((name) => (
{["OpenAI", "OpenAI Responses", "Claude", "Gemini", "Azure OpenAI"].map((name) => (
<div
key={name}
className="flex items-center gap-2 text-sm font-medium hover:text-muted-foreground transition-colors"
+1 -1
View File
@@ -11,7 +11,7 @@ export interface ChannelModelMapping {
export interface ChannelConfig {
name: string
type: 'openai' | 'azure-openai' | 'openai-audio' | 'azure-openai-audio' | 'claude' | 'claude-to-openai' | 'openai-responses' | 'azure-openai-responses'
type: 'openai' | 'gemini' | 'azure-openai' | 'openai-audio' | 'azure-openai-audio' | 'claude' | 'claude-to-openai' | 'openai-responses' | 'azure-openai-responses'
endpoint: string
api_key?: string
api_keys?: string[]
+2 -1
View File
@@ -90,11 +90,12 @@ const buildModelsFetchRequest = (
headers.set("anthropic-version", DEFAULT_CLAUDE_API_VERSION);
break;
case "openai":
case "gemini":
case "openai-audio":
case "openai-responses":
case "claude-to-openai":
default:
targetUrl = buildPrefixedTargetUrl(config.endpoint, "/v1/models");
targetUrl = buildPrefixedTargetUrl(config.endpoint, "/v1/models", "/v1", config.type);
headers.set("Authorization", `Bearer ${apiKey}`);
break;
}
+1 -1
View File
@@ -8,7 +8,7 @@ const buildProxyRequest = (
config: ChannelConfig
): Request => {
const url = new URL(request.url)
const targetUrl = buildPrefixedTargetUrl(config.endpoint, url.pathname)
const targetUrl = buildPrefixedTargetUrl(config.endpoint, url.pathname, "/v1", config.type)
const apiKey = config.api_key || ""
const targetHeaders = new Headers(request.headers)
+26 -1
View File
@@ -10,10 +10,27 @@ const joinPath = (...segments: string[]): string => {
return `/${cleanedSegments.join("/")}`;
};
const normalizeGeminiBasePath = (basePath: string): string => {
if (basePath.endsWith("v1beta/openai")) {
return basePath;
}
if (basePath.endsWith("v1beta")) {
return joinPath(basePath, "openai");
}
if (basePath.endsWith("openai")) {
return basePath;
}
return joinPath(basePath, "v1beta/openai");
};
export const buildPrefixedTargetUrl = (
endpoint: string,
requestPath: string,
prefixToStrip = "/v1"
prefixToStrip = "/v1",
providerType?: string | null,
): URL => {
const targetUrl = new URL(endpoint);
@@ -22,6 +39,14 @@ export const buildPrefixedTargetUrl = (
}
const currentBasePath = trimSlashes(targetUrl.pathname);
if (providerType === "gemini") {
const geminiBasePath = normalizeGeminiBasePath(currentBasePath);
const normalizedRequestPath = requestPath.replace(/^\/v1(?=\/|$)/, "");
targetUrl.pathname = joinPath(geminiBasePath, normalizedRequestPath);
return targetUrl;
}
const normalizedPrefix = trimSlashes(prefixToStrip);
const baseAlreadyContainsPrefix = normalizedPrefix.length > 0
&& currentBasePath.endsWith(normalizedPrefix);
@@ -17,6 +17,7 @@ export type ProviderFetch = (
const providerMap: Record<string, ProviderFetch> = {
"azure-openai": azureOpenaiProxy.fetch,
"openai": openaiProxy.fetch,
"gemini": openaiProxy.fetch,
"azure-openai-audio": azureOpenaiProxy.fetch,
"openai-audio": openaiProxy.fetch,
"claude": claudeProxy.fetch,
+7 -3
View File
@@ -4,16 +4,20 @@ type RoutePolicy = {
allowedTypes: ChannelType[] | null
}
const TEXT_CHANNEL_TYPES: ChannelType[] = [
const CHAT_COMPLETIONS_CHANNEL_TYPES: ChannelType[] = [
"openai",
"azure-openai",
"gemini",
]
const MESSAGES_CHANNEL_TYPES: ChannelType[] = [
"claude",
"claude-to-openai",
]
const ROUTE_POLICIES: Record<RouteId, RoutePolicy> = {
"chat-completions": { allowedTypes: TEXT_CHANNEL_TYPES },
"messages": { allowedTypes: TEXT_CHANNEL_TYPES },
"chat-completions": { allowedTypes: CHAT_COMPLETIONS_CHANNEL_TYPES },
"messages": { allowedTypes: MESSAGES_CHANNEL_TYPES },
"responses": { allowedTypes: ["openai-responses", "azure-openai-responses"] },
"audio-speech": { allowedTypes: ["openai-audio", "azure-openai-audio"] },
}
Vendored
+1
View File
@@ -36,6 +36,7 @@ type ApiTokenRow = BaseDbRow & {
type ChannelType =
| "azure-openai"
| "openai"
| "gemini"
| "azure-openai-audio"
| "openai-audio"
| "claude"