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;