feat: add OpenAI provider support

This commit is contained in:
dreamhunter2333 committed 2025-06-26 01:24:43 +08:00
1 parent d62e662d20
commit d4d318579b
5 files changed
+180 -20

No files matched your search

+37 -14
View File
@@ -5,7 +5,7 @@
## ✨ 特性
- 🚀 **基于 Cloudflare Workers**:无服务器架构,全球边缘部署
- 🔐 **多渠道支持**:目前支持 Azure OpenAI,后续会继续增加其他 AI 服务提供商
- 🔐 **多渠道支持**:支持 Azure OpenAI 和 OpenAI,后续会继续增加其他 AI 服务提供商
- 🎫 **Token 管理**:完整的 API Token 生成、管理和配额控制
- 📊 **使用量统计**:实时统计 API 使用量和费用
- 💰 **定价管理**:灵活的模型定价配置
@@ -26,6 +26,7 @@ awsl-one-api/
│ │ └── index.ts # 管理接口路由
│ ├── providers/ # AI 服务提供商
│ │ ├── azure-openai-proxy.ts # Azure OpenAI 代理
│ │ ├── openai-proxy.ts # OpenAI 代理
│ │ └── index.ts # 提供商路由
│ ├── db/ # 数据库相关
│ ├── model/ # 数据模型
@@ -44,9 +45,9 @@ awsl-one-api/
```text
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Client Apps │───▶│ AWSL One API │───▶│ AI Providers │
│ │ │ (CF Workers) │ │ (Azure OpenAI) │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│
│ │ │ (CF Workers) │ │(Azure OpenAI & │
└─────────────────┘ └─────────────────┘ │ OpenAI) │
│ └─────────────────┘
▼
┌─────────────────┐
│ Cloudflare D1 │
@@ -127,8 +128,13 @@ pnpm run deploy
1. 在 Web 界面切换到 **🔗 频道管理** 标签
2. 点击 **➕ 添加频道** 按钮
3. 填写频道标识和配置信息
4. 点击 **💾 保存频道** 按钮
3. 选择频道类型(Azure OpenAI 或 OpenAI)
4. 填写频道标识和配置信息:
- 对于 OpenAI:填写基本信息(名称、端点、API密钥)和模型映射配置
- 对于 Azure OpenAI:还需要配置 API 版本和模型部署映射
5. 点击 **💾 保存频道** 按钮
**提示**:系统会根据选择的频道类型自动显示相应的配置字段。
### Token 创建和使用
@@ -185,7 +191,7 @@ curl https://your-domain.com/v1/chat/completions \
访问 `https://your-domain.com` 即可使用 Web 管理界面,功能包括:
- **📊 数据库管理**:一键初始化数据库表结构
- **🔗 频道配置管理**:添加、编辑、删除 AI 服务提供商频道
- **🔗 频道配置管理**:添加、编辑、删除 AI 服务提供商频道(支持 Azure OpenAI 和 OpenAI)
- **🔑 API Token 管理**:生成、管理和监控 API Token 使用情况
- **💰 定价配置**:灵活配置不同模型的定价策略
- **🧪 API 测试工具**:内置 API 测试界面,支持实时调试和错误排查
@@ -194,21 +200,23 @@ curl https://your-domain.com/v1/chat/completions \
- **现代化 UI**:响应式设计,支持桌面和移动设备
- **实时反馈**:操作结果即时显示,支持悬浮提示
- **智能表单**:自动生成 Token、JSON 格式验证、一键复制功能
- **智能表单**:自动生成 Token、JSON 格式验证、一键复制功能,根据频道类型智能显示配置字段
- **安全认证**:管理员 Token 认证,数据安全保护
## 🔧 配置说明
### 渠道配置
目前支持 Azure OpenAI 服务提供商的配置:
目前支持以下 AI 服务提供商:
#### Azure OpenAI 配置
```json
{
"name": "Azure OpenAI",
"name": "My Azure OpenAI",
"type": "azure-openai",
"endpoint": "https://your-resource.openai.azure.com/",
"api_key": "your-api-key",
"api_key": "your-azure-api-key",
"api_version": "2024-02-15-preview",
"deployment_mapper": {
"gpt-4": "gpt-4-deployment-name",
@@ -217,14 +225,29 @@ curl https://your-domain.com/v1/chat/completions \
}
```
#### OpenAI 配置
```json
{
"name": "My OpenAI Channel",
"type": "openai",
"endpoint": "https://api.openai.com/v1/",
"api_key": "sk-your-openai-api-key",
"deployment_mapper": {
"gpt-4": "gpt-4",
"gpt-3.5-turbo": "gpt-3.5-turbo"
}
}
```
**配置说明**:
- `name`: 频道显示名称
- `type`: 服务提供商类型(如 `azure-openai`)
- `type`: 服务提供商类型(`azure-openai` 或 `openai`)
- `endpoint`: API 端点地址
- `api_key`: API 密钥
- `api_version`: API 版本
- `deployment_mapper`: 模型名称映射关系
- `api_version`: API 版本(仅 Azure OpenAI 需要)
- `deployment_mapper`: 模型名称映射关系(用于自定义模型名称映射)
### Token 配置
+6 -4
View File
@@ -842,12 +842,14 @@
<div id="channelFormMode">
<div class="form-group">
<label for="channelName">频道名称</label>
<input type="text" id="channelName" class="form-control" placeholder="例如: Azure OpenAI">
<input type="text" id="channelName" class="form-control"
placeholder="例如: My OpenAI Channel">
</div>
<div class="form-group">
<label for="channelType">频道类型</label>
<select id="channelType" class="form-control" onchange="toggleApiVersionField()">
<option value="azure-openai">Azure OpenAI</option>
<option value="openai">OpenAI</option>
</select>
</div>
<div class="form-group">
@@ -869,7 +871,7 @@
<label for="deploymentMapper">模型部署映射 (JSON)</label>
<textarea id="deploymentMapper" class="form-control" rows="4"
placeholder='{"gpt-35-turbo": "gpt-35-turbo", "gpt-4": "gpt-4"}'></textarea>
<small style="color: #666; font-size: 12px;">配置模型名称到部署名称的映射关系</small>
<small style="color: #666; font-size: 12px;">配置模型名称映射关系(主要用于Azure OpenAI)</small>
</div>
<div class="form-group">
<label for="additionalConfig">其他配置 (JSON,可选)</label>
@@ -1695,8 +1697,8 @@
// 重置表单状态
FormUtils.setReadonly('channelKey', false);
DOMUtils.setValue('channelType', 'azure-openai');
DOMUtils.getElement('channelKey').placeholder = '例如: azure-openai-1';
DOMUtils.setValue('channelType', 'openai');
DOMUtils.getElement('channelKey').placeholder = '例如: openai-1';
// 重置编辑模式
channelModeManager.resetToFormMode();
+2 -1
View File
@@ -2,9 +2,9 @@ import { Context, Hono } from "hono"
import { contentJson, fromHono, OpenAPIRoute } from 'chanfana';
import azureOpenaiProxy from "./azure-openai-proxy"
import openaiProxy from "./openai-proxy"
import utils from "../utils"
import { TokenUtils } from "../admin/token_utils"
import { ChannelConfigRow } from "../db/db_model"
import { CONSTANTS } from "../constants"
import { z } from "zod";
@@ -19,6 +19,7 @@ const providerMap: Record<
) => Promise<Response>
> = {
"azure-openai": azureOpenaiProxy.fetch,
"openai": openaiProxy.fetch,
};
class ProxyEndpoint extends OpenAPIRoute {
+134
View File
@@ -0,0 +1,134 @@
import { Context } from "hono"
import { streamText } from 'hono/streaming'
// 辅助函数
const buildProxyRequest = (
request: Request,
reqJson: any,
config: ChannelConfig
): Request => {
const url = new URL(request.url)
const targetUrl = new URL(config.endpoint)
targetUrl.pathname = url.pathname
const targetHeaders = new Headers(request.headers)
targetHeaders.set("Authorization", `Bearer ${config.api_key}`)
return new Request(targetUrl, {
method: request.method,
headers: targetHeaders,
body: JSON.stringify(reqJson),
})
}
const checkoutUsageData = async (
saveUsage: (usage: Usage) => Promise<void>,
response: Response | OpenAIResponse
): Promise<void> => {
try {
const res = response instanceof Response
? await response.clone().json<OpenAIResponse>()
: response
if (!res.usage) return;
await saveUsage(res.usage)
} catch (error) {
console.error("Error logging usage data:", error)
}
}
const processStreamData = (
lines: string[],
saveUsage: (usage: Usage) => Promise<void>
): void => {
lines
.map(line => line.trim())
.filter(line => line.length > 0 && line.startsWith('data:'))
.map(line => line.replace('data: ', '').trim())
.filter(line => line !== '[DONE]')
.forEach(jsonContent => {
try {
const res = JSON.parse(jsonContent) as OpenAIResponse
checkoutUsageData(saveUsage, res)
} catch (e) {
console.error("Error parsing stream data:", e)
}
})
}
const handleStreamResponse = async (
c: Context<HonoCustomType>,
response: Response,
saveUsage: (usage: Usage) => Promise<void>
): Promise<Response> => {
return streamText(c, async (stream) => {
const reader = response.body?.getReader()
if (!reader) {
throw new Error("No reader found in response body")
}
const decoder = new TextDecoder('utf-8')
let buffer = ""
while (true) {
const { done, value } = await reader.read()
if (done) break
const chunk = decoder.decode(value, { stream: true })
await stream.write(chunk)
buffer += chunk
if (!chunk.includes('\n')) continue
const lines = buffer.split('\n')
buffer = lines.pop() || ""
processStreamData(lines, saveUsage)
}
// 处理最后剩余的数据
if (buffer.trim()) {
processStreamData([buffer], saveUsage)
}
})
}
export default {
async fetch(
c: Context<HonoCustomType>,
config: ChannelConfig,
saveUsage: (usage: Usage) => Promise<void>,
): Promise<Response> {
// 准备请求数据
const reqJson = await c.req.json()
// 强制包含使用数据
const { model: modelName, stream } = reqJson;
if (stream) {
reqJson.stream_options = { "include_usage": true }
}
// 检查模型是否受支持
const deploymentName = config.deployment_mapper[modelName]
if (!deploymentName) {
throw new Error(`Model ${modelName} not supported`)
}
// 构建目标请求
const proxyRequest = buildProxyRequest(c.req.raw, reqJson, config)
// 发送请求并获取响应
const response = await fetch(proxyRequest)
// 处理流式响应
if (stream) {
return handleStreamResponse(c, response, saveUsage)
}
// 处理常规响应
if (response.ok) {
await checkoutUsageData(saveUsage, response)
}
return response
}
}
Vendored
+1 -1
View File
@@ -32,7 +32,7 @@ type ApiTokenRow = BaseDbRow & {
usage: number;
}
type ChannelType = "azure-openai" | undefined | null;
type ChannelType = "azure-openai" | "openai" | undefined | null;
type ChannelConfig = {
name: string;