feat: add OpenAI provider support
This commit is contained in:
1 parent
d62e662d20
commit
d4d318579b
5 files changed
+180
-20
No files matched your search
@@ -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
@@ -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,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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user