feat(channels): 添加对 Gemini 渠道的支持和相关配置
This commit is contained in:
1 parent
88ae321d4f
commit
7707de2b3e
10 files changed
+108
-18
No files matched your search
@@ -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 请留空)
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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[]
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"] },
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user