diff --git a/README.md b/README.md
index 21bbb9e..d65ec41 100644
--- a/README.md
+++ b/README.md
@@ -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 配置
diff --git a/public/index.html b/public/index.html
index cf36e74..930f714 100644
--- a/public/index.html
+++ b/public/index.html
@@ -842,12 +842,14 @@
-
+
@@ -869,7 +871,7 @@
- 配置模型名称到部署名称的映射关系
+ 配置模型名称映射关系(主要用于Azure OpenAI)
@@ -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();
diff --git a/src/providers/index.ts b/src/providers/index.ts
index bca8b0f..bc98aa7 100644
--- a/src/providers/index.ts
+++ b/src/providers/index.ts
@@ -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
> = {
"azure-openai": azureOpenaiProxy.fetch,
+ "openai": openaiProxy.fetch,
};
class ProxyEndpoint extends OpenAPIRoute {
diff --git a/src/providers/openai-proxy.ts b/src/providers/openai-proxy.ts
new file mode 100644
index 0000000..9db9717
--- /dev/null
+++ b/src/providers/openai-proxy.ts
@@ -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,
+ response: Response | OpenAIResponse
+): Promise => {
+ try {
+ const res = response instanceof Response
+ ? await response.clone().json()
+ : 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 => {
+ 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,
+ response: Response,
+ saveUsage: (usage: Usage) => Promise
+): Promise => {
+ 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,
+ config: ChannelConfig,
+ saveUsage: (usage: Usage) => Promise,
+ ): Promise {
+ // 准备请求数据
+ 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
+ }
+}
diff --git a/type.d.ts b/type.d.ts
index 460d06b..ace3584 100644
--- a/type.d.ts
+++ b/type.d.ts
@@ -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;