clash-linux commited on
Commit
552a548
·
verified ·
1 Parent(s): e0b237a

Upload 15 files

Browse files
Files changed (3) hide show
  1. .gitattributes +35 -35
  2. README.md +176 -176
  3. src/routes/chat.js +476 -472
.gitattributes CHANGED
@@ -1,35 +1,35 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
- *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
- *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -1,176 +1,176 @@
1
- ---
2
- title: PromptLayer API Proxy
3
- emoji: 🚀
4
- colorFrom: blue
5
- colorTo: green
6
- sdk: docker
7
- app_port: 3000
8
- ---
9
- <div align="center">
10
-
11
- # 🚀 PromptLayer API 代理服务
12
-
13
- [![GitHub stars](https://img.shields.io/github/stars/Rfym21/PromptlayerProxy?style=social)](https://github.com/Rfym21/PromptlayerProxy)
14
- [![Docker Pulls](https://img.shields.io/docker/pulls/rfym21/promptlayer-proxy)](https://hub.docker.com/r/rfym21/promptlayer-proxy)
15
-
16
- *一个强大的 PromptLayer API 代理服务,支持多种主流 AI 模型*
17
-
18
- **🔗 [交流群](https://t.me/nodejs_project) | 🐳 [Docker Hub](https://hub.docker.com/r/rfym21/promptlayer-proxy)**
19
-
20
- </div>
21
-
22
- ## ✨ 功能特点
23
-
24
- <div align="center">
25
-
26
- | 功能 | 状态 | 描述 |
27
- |------|------|------|
28
- | 🔄 **OpenAI API 兼容** | ✅ | 完全兼容 OpenAI API 格式 |
29
- | 🌊 **流式输出** | ✅ | 支持实时流式响应 |
30
- | 🖼️ **图像处理** | ✅ | 支持图像上传和识别 |
31
- | ⚖️ **负载均衡** | ✅ | 多账户轮询负载均衡 |
32
- | 🐳 **容器化部署** | ✅ | Docker 一键部署 |
33
- | 🔄 **自动刷新** | ✅ | 智能 Token 自动刷新 |
34
- | 🛠️ **Tools 支持** | ✅ | 支持Tools参数 |
35
- | 🔌 **其他参数(温度,Max_Tokens)** | ✅ | 支持配置其他参数,设置的参数将覆盖默认参数 |
36
-
37
- </div>
38
-
39
- ---
40
-
41
- ## 🤖 支持的模型
42
-
43
- <div align="center">
44
-
45
- | 🏷️ 模型名称 | 📊 最大输出长度 | 🧠 思考长度 | 📈 类型 |
46
- |-----------|-------------|---------|-------|
47
- | 🔮 `claude-3-7-sonnet-20250219` | `64,000` | `-` | Anthropic |
48
- | 🧠 `claude-3-7-sonnet-20250219-thinking` | `64,000` | `32,000` | Anthropic |
49
- | 🔮 `claude-sonnet-4-20250514` | `64,000` | `-` | Anthropic |
50
- | 🧠 `claude-sonnet-4-20250514-thinking` | `64,000` | `32,000` | Anthropic |
51
- | 🔮 `claude-opus-4-20250514` | `32,000` | `-` | Anthropic |
52
- | 🧠 `claude-opus-4-20250514-thinking` | `32,000` | `16,000` | Anthropic |
53
- | 🤖 `o4-mini` | `100,000` | `-` | OpenAI |
54
- | 🤖 `chatgpt-4o-latest` | `-` | `-` | OpenAI |
55
- | 🤖 `gpt-4.1` | `-` | `-` | OpenAI |
56
- | 🤖 `gpt-4.5-preview` | `-` | `-` | OpenAI |
57
-
58
- </div>
59
-
60
- ---
61
-
62
- ## 🚀 快速开始
63
-
64
- ### 方式一:🐳 Docker Compose(推荐)
65
-
66
- #### 📥 **Step 1**: 下载配置文件
67
-
68
- ```bash
69
- curl -o docker-compose.yml https://raw.githubusercontent.com/Rfym21/PromptlayerProxy/refs/heads/main/docker-compose.yml
70
- ```
71
-
72
- #### ⚙️ **Step 2**: 配置环境变量
73
-
74
- 在 `docker-compose.yml` 文件中设置以下参数:
75
-
76
- ```yaml
77
- services:
78
- promptlayer-proxy:
79
- image: rfym21/promptlayer-proxy:latest
80
- container_name: promptlayer-proxy
81
- restart: always
82
- ports:
83
- - "3000:3000"
84
- environment:
85
- # 🔐 PromptLayer 账号密码
86
- - ACCOUNTS=your_account1:your_password1,your_account2:your_password2...
87
- # 🔑 API 认证密钥
88
- - AUTH_TOKEN=your_auth_token_here
89
- ```
90
-
91
- #### 🚀 **Step 3**: 启动服务
92
-
93
- ```bash
94
- docker-compose up -d
95
- ```
96
-
97
- ---
98
-
99
- ### 方式二:🐳 Docker CLI
100
-
101
- ```bash
102
- docker run -d \
103
- --name promptlayer-proxy \
104
- -p 3000:3000 \
105
- -e ACCOUNTS=your_account:your_password \
106
- -e AUTH_TOKEN=your_auth_token_here \
107
- rfym21/promptlayer-proxy:latest
108
- ```
109
-
110
- ---
111
-
112
- ### 方式三:🤗 Hugging Face Spaces
113
-
114
- 1. **创建 Space**:
115
- * 访问 [Hugging Face Spaces](https://huggingface.co/new-space) 并创建一个新的 Space。
116
- * 选择 "Docker" 作为 SDK。
117
- * 选择适合的硬件(通常免费套餐已足够)。
118
-
119
- 2. **配置 Secrets**:
120
- * 在 Space 的 "Settings" 标签页中,找到 "Secrets and variables" 部分。
121
- * 添加以下 Secrets:
122
- * `ACCOUNTS`: 你的 PromptLayer 账号和密码,格式同 Docker 部署方式,例如 `your_account1:your_password1,your_account2:your_password2`。
123
- * `AUTH_TOKEN`: 你设置的 API 认证密钥。
124
-
125
- 3. **上传文件**:
126
- * 将本项目的所有文件(包括 `Dockerfile`, `package.json`, `src` 目录等)上传到你的 Space Git 仓库中。
127
- * 确保 `README.md` 文件头部的 YAML 配置如下(如果通过 Hugging Face 界面创建 Docker Space,会自动生成类似配置,请确保 `app_port` 为 `3000`):
128
- ```yaml
129
- ---
130
- title: PromptLayer API Proxy
131
- emoji: 🚀
132
- colorFrom: blue
133
- colorTo: green
134
- sdk: docker
135
- app_port: 3000
136
- ---
137
- ```
138
-
139
- 4. **部署**:
140
- * Hugging Face Spaces 会自动根据 `Dockerfile` 构建并部署你的应用。
141
- * 部署完成后,你的服务将在 Space 的 URL 上可用。
142
-
143
- ---
144
-
145
- ### 方式四:💻 本地开发
146
-
147
- #### 📦 **Step 1**: 安装依赖
148
-
149
- ```bash
150
- npm install
151
- ```
152
-
153
- #### 📝 **Step 2**: 环境配置
154
-
155
- 创建 `.env` 文件:
156
-
157
- ```env
158
- ACCOUNTS=your_account:your_password
159
- AUTH_TOKEN=your_auth_token_here
160
- ```
161
-
162
- #### 🏃 **Step 3**: 启动开发模式
163
-
164
- ```bash
165
- npm run dev
166
- ```
167
-
168
- ---
169
-
170
- <div align="center">
171
-
172
- ## 💬 交流与支持
173
-
174
- [![Telegram](https://img.shields.io/badge/Telegram-2CA5E0?style=for-the-badge&logo=telegram&logoColor=white)](https://t.me/nodejs_project)
175
-
176
- </div>
 
1
+ ---
2
+ title: PromptLayer API Proxy
3
+ emoji: 🚀
4
+ colorFrom: blue
5
+ colorTo: green
6
+ sdk: docker
7
+ app_port: 3000
8
+ ---
9
+ <div align="center">
10
+
11
+ # 🚀 PromptLayer API 代理服务
12
+
13
+ [![GitHub stars](https://img.shields.io/github/stars/Rfym21/PromptlayerProxy?style=social)](https://github.com/Rfym21/PromptlayerProxy)
14
+ [![Docker Pulls](https://img.shields.io/docker/pulls/rfym21/promptlayer-proxy)](https://hub.docker.com/r/rfym21/promptlayer-proxy)
15
+
16
+ *一个强大的 PromptLayer API 代理服务,支持多种主流 AI 模型*
17
+
18
+ **🔗 [交流群](https://t.me/nodejs_project) | 🐳 [Docker Hub](https://hub.docker.com/r/rfym21/promptlayer-proxy)**
19
+
20
+ </div>
21
+
22
+ ## ✨ 功能特点
23
+
24
+ <div align="center">
25
+
26
+ | 功能 | 状态 | 描述 |
27
+ |------|------|------|
28
+ | 🔄 **OpenAI API 兼容** | ✅ | 完全兼容 OpenAI API 格式 |
29
+ | 🌊 **流式输出** | ✅ | 支持实时流式响应 |
30
+ | 🖼️ **图像处理** | ✅ | 支持图像上传和识别 |
31
+ | ⚖️ **负载均衡** | ✅ | 多账户轮询负载均衡 |
32
+ | 🐳 **容器化部署** | ✅ | Docker 一键部署 |
33
+ | 🔄 **自动刷新** | ✅ | 智能 Token 自动刷新 |
34
+ | 🛠️ **Tools 支持** | ✅ | 支持Tools参数 |
35
+ | 🔌 **其他参数(温度,Max_Tokens)** | ✅ | 支持配置其他参数,设置的参数将覆盖默认参数 |
36
+
37
+ </div>
38
+
39
+ ---
40
+
41
+ ## 🤖 支持的模型
42
+
43
+ <div align="center">
44
+
45
+ | 🏷️ 模型名称 | 📊 最大输出长度 | 🧠 思考长度 | 📈 类型 |
46
+ |-----------|-------------|---------|-------|
47
+ | 🔮 `claude-3-7-sonnet-20250219` | `64,000` | `-` | Anthropic |
48
+ | 🧠 `claude-3-7-sonnet-20250219-thinking` | `64,000` | `32,000` | Anthropic |
49
+ | 🔮 `claude-sonnet-4-20250514` | `64,000` | `-` | Anthropic |
50
+ | 🧠 `claude-sonnet-4-20250514-thinking` | `64,000` | `32,000` | Anthropic |
51
+ | 🔮 `claude-opus-4-20250514` | `32,000` | `-` | Anthropic |
52
+ | 🧠 `claude-opus-4-20250514-thinking` | `32,000` | `16,000` | Anthropic |
53
+ | 🤖 `o4-mini` | `100,000` | `-` | OpenAI |
54
+ | 🤖 `chatgpt-4o-latest` | `-` | `-` | OpenAI |
55
+ | 🤖 `gpt-4.1` | `-` | `-` | OpenAI |
56
+ | 🤖 `gpt-4.5-preview` | `-` | `-` | OpenAI |
57
+
58
+ </div>
59
+
60
+ ---
61
+
62
+ ## 🚀 快速开始
63
+
64
+ ### 方式一:🐳 Docker Compose(推荐)
65
+
66
+ #### 📥 **Step 1**: 下载配置文件
67
+
68
+ ```bash
69
+ curl -o docker-compose.yml https://raw.githubusercontent.com/Rfym21/PromptlayerProxy/refs/heads/main/docker-compose.yml
70
+ ```
71
+
72
+ #### ⚙️ **Step 2**: 配置环境变量
73
+
74
+ 在 `docker-compose.yml` 文件中设置以下参数:
75
+
76
+ ```yaml
77
+ services:
78
+ promptlayer-proxy:
79
+ image: rfym21/promptlayer-proxy:latest
80
+ container_name: promptlayer-proxy
81
+ restart: always
82
+ ports:
83
+ - "3000:3000"
84
+ environment:
85
+ # 🔐 PromptLayer 账号密码
86
+ - ACCOUNTS=your_account1:your_password1,your_account2:your_password2...
87
+ # 🔑 API 认证密钥
88
+ - AUTH_TOKEN=your_auth_token_here
89
+ ```
90
+
91
+ #### 🚀 **Step 3**: 启动服务
92
+
93
+ ```bash
94
+ docker-compose up -d
95
+ ```
96
+
97
+ ---
98
+
99
+ ### 方式二:🐳 Docker CLI
100
+
101
+ ```bash
102
+ docker run -d \
103
+ --name promptlayer-proxy \
104
+ -p 3000:3000 \
105
+ -e ACCOUNTS=your_account:your_password \
106
+ -e AUTH_TOKEN=your_auth_token_here \
107
+ rfym21/promptlayer-proxy:latest
108
+ ```
109
+
110
+ ---
111
+
112
+ ### 方式三:🤗 Hugging Face Spaces
113
+
114
+ 1. **创建 Space**:
115
+ * 访问 [Hugging Face Spaces](https://huggingface.co/new-space) 并创建一个新的 Space。
116
+ * 选择 "Docker" 作为 SDK。
117
+ * 选择适合的硬件(通常免费套餐已足够)。
118
+
119
+ 2. **配置 Secrets**:
120
+ * 在 Space 的 "Settings" 标签页中,找到 "Secrets and variables" 部分。
121
+ * 添加以下 Secrets:
122
+ * `ACCOUNTS`: 你的 PromptLayer 账号和密码,格式同 Docker 部署方式,例如 `your_account1:your_password1,your_account2:your_password2`。
123
+ * `AUTH_TOKEN`: 你设置的 API 认证密钥。
124
+
125
+ 3. **上传文件**:
126
+ * 将本项目的所有文件(包括 `Dockerfile`, `package.json`, `src` 目录等)上传到你的 Space Git 仓库中。
127
+ * 确保 `README.md` 文件头部的 YAML 配置如下(如果通过 Hugging Face 界面创建 Docker Space,会自动生成类似配置,请确保 `app_port` 为 `3000`):
128
+ ```yaml
129
+ ---
130
+ title: PromptLayer API Proxy
131
+ emoji: 🚀
132
+ colorFrom: blue
133
+ colorTo: green
134
+ sdk: docker
135
+ app_port: 3000
136
+ ---
137
+ ```
138
+
139
+ 4. **部署**:
140
+ * Hugging Face Spaces 会自动根据 `Dockerfile` 构建并部署你的应用。
141
+ * 部署完成后,你的服务将在 Space 的 URL 上可用。
142
+
143
+ ---
144
+
145
+ ### 方式四:💻 本地开发
146
+
147
+ #### 📦 **Step 1**: 安装依赖
148
+
149
+ ```bash
150
+ npm install
151
+ ```
152
+
153
+ #### 📝 **Step 2**: 环境配置
154
+
155
+ 创建 `.env` 文件:
156
+
157
+ ```env
158
+ ACCOUNTS=your_account:your_password
159
+ AUTH_TOKEN=your_auth_token_here
160
+ ```
161
+
162
+ #### 🏃 **Step 3**: 启动开发模式
163
+
164
+ ```bash
165
+ npm run dev
166
+ ```
167
+
168
+ ---
169
+
170
+ <div align="center">
171
+
172
+ ## 💬 交流与支持
173
+
174
+ [![Telegram](https://img.shields.io/badge/Telegram-2CA5E0?style=for-the-badge&logo=telegram&logoColor=white)](https://t.me/nodejs_project)
175
+
176
+ </div>
src/routes/chat.js CHANGED
@@ -1,472 +1,476 @@
1
- const express = require('express')
2
- const axios = require('axios')
3
- const WebSocket = require('ws')
4
- const router = express.Router()
5
- const { v4: uuidv4 } = require('uuid')
6
- const { uploadFileBuffer } = require('../lib/upload')
7
- const verify = require('./verify')
8
- const modelMap = require('../lib/model-map')
9
-
10
-
11
- async function parseMessages(req, res, next) {
12
- const messages = req.body.messages
13
- if (!Array.isArray(messages)) {
14
- req.processedMessages = []
15
- return next()
16
- }
17
-
18
- try {
19
- const transformedMessages = await Promise.all(messages.map(async (msg) => {
20
- // Determine provider to conditionally convert system role for Anthropic
21
- const modelName = req.body.model;
22
- const modelData = modelMap[modelName]; // modelMap is required at the top
23
- const provider = modelData?.provider; // Use optional chaining for safety
24
-
25
- let message = {
26
- role: (msg.role === "system" && provider === "anthropic") ? "user" : msg.role,
27
- tool_calls: [],
28
- template_format: "jinja2"
29
- }
30
-
31
- if (Array.isArray(msg.content)) {
32
- const contentItems = await Promise.all(msg.content.map(async (item) => {
33
- if (item.type === "text") {
34
- return {
35
- type: "text",
36
- text: item.text
37
- }
38
- }
39
- else if (item.type === "image_url") {
40
- try {
41
- const base64Match = item.image_url.url.match(/^data:image\/\w+;base64,(.+)$/)
42
- if (base64Match) {
43
- const base64 = base64Match[1]
44
- const data = Buffer.from(base64, 'base64')
45
- const uploadResult = await uploadFileBuffer(data)
46
-
47
- return {
48
- type: "media",
49
- media: {
50
- "type": "image",
51
- "url": uploadResult.file_url,
52
- "title": `image_${Date.now()}.png`
53
- }
54
- }
55
- } else {
56
- return {
57
- type: "media",
58
- media: {
59
- "type": "image",
60
- "url": item.image_url.url,
61
- "title": "external_image"
62
- }
63
- }
64
- }
65
- } catch (error) {
66
- console.error("处理图像时出错:", error)
67
- return {
68
- type: "text",
69
- text: "[图像处理失败]"
70
- }
71
- }
72
- } else {
73
- return {
74
- type: "text",
75
- text: JSON.stringify(item)
76
- }
77
- }
78
- }))
79
-
80
- message.content = contentItems
81
- } else {
82
- message.content = [
83
- {
84
- type: "text",
85
- text: msg.content || ""
86
- }
87
- ]
88
- }
89
-
90
- return message
91
- }))
92
-
93
- req.body.messages = transformedMessages
94
- return next()
95
- } catch (error) {
96
- console.error("处理消息时出错:", error.status)
97
- req.body.messages = []
98
- return next(error)
99
- }
100
- }
101
-
102
- async function getChatID(req, res) {
103
- try {
104
- const url = 'https://api.promptlayer.com/api/dashboard/v2/workspaces/' + req.account.workspaceId + '/playground_sessions'
105
- const headers = { Authorization: "Bearer " + req.account.token }
106
- const model_data = modelMap[req.body.model] ? modelMap[req.body.model] : modelMap["claude-3-7-sonnet-20250219"]
107
- let data = {
108
- "id": uuidv4(),
109
- "name": "Not implemented",
110
- "prompt_blueprint": {
111
- "inference_client_name": null,
112
- "metadata": {
113
- "model": model_data
114
- },
115
- "prompt_template": {
116
- "type": "chat",
117
- "messages": req.body.messages,
118
- "tools": req.body.tools || [],
119
- "tool_choice": req.body.tool_choice || "none",
120
- "input_variables": [],
121
- "functions": [],
122
- "function_call": null
123
- },
124
- "provider_base_url_name": null
125
- },
126
- "input_variables": []
127
- }
128
-
129
- for (const item in req.body) {
130
- if (item === "messages" || item === "model" || item === "stream") {
131
- continue
132
- } else if (model_data.parameters[item]) {
133
- model_data.parameters[item] = req.body[item]
134
- }
135
- }
136
- data.prompt_blueprint.metadata.model = model_data
137
- console.log(`模型参数: ${data.prompt_blueprint.metadata.model}`)
138
-
139
- const response = await axios.put(url, data, { headers })
140
- if (response.data.success) {
141
- console.log(`生成会话ID成功: ${response.data.playground_session.id}`)
142
- req.chatID = response.data.playground_session.id
143
- return response.data.playground_session.id
144
- } else {
145
- return false
146
- }
147
- } catch (error) {
148
- // console.error("错误:", error.response?.data)
149
- res.status(500).json({
150
- "error": {
151
- "message": error.message || "服务器内部错误",
152
- "type": "server_error",
153
- "param": null,
154
- "code": "server_error"
155
- }
156
- })
157
- return false
158
- }
159
- }
160
-
161
- async function sentRequest(req, res) {
162
- try {
163
- const url = 'https://api.promptlayer.com/api/dashboard/v2/workspaces/' + req.account.workspaceId + '/run_groups'
164
- const headers = { Authorization: "Bearer " + req.account.token }
165
- const model_data = modelMap[req.body.model] ? modelMap[req.body.model] : modelMap["claude-3-7-sonnet-20250219"];
166
- const provider = model_data?.provider; // Get provider
167
-
168
- // Base prompt template structure
169
- let prompt_template = {
170
- "type": "chat",
171
- "messages": req.body.messages,
172
- "tools": req.body.tools || [], // Default value
173
- "tool_choice": req.body.tool_choice || "none", // Default value
174
- "input_variables": [],
175
- "functions": [],
176
- "function_call": null
177
- };
178
-
179
- // Conditionally modify for Mistral/Cohere
180
- if (provider === 'mistral' || provider === 'cohere') {
181
- prompt_template.tools = null;
182
- delete prompt_template.tool_choice; // Remove tool_choice entirely
183
- delete prompt_template.function_call;
184
- }
185
-
186
- let data = {
187
- "id": uuidv4(),
188
- "playground_session_id": req.chatID,
189
- "shared_prompt_blueprint": {
190
- "inference_client_name": null,
191
- "metadata": {
192
- "model": model_data // Keep original model_data here for metadata
193
- },
194
- "prompt_template": prompt_template, // Use the adjusted template
195
- "provider_base_url_name": null
196
- },
197
- "individual_run_requests": [
198
- {
199
- "input_variables": {},
200
- "run_group_position": 1
201
- }
202
- ]
203
- };
204
-
205
- console.log(JSON.stringify(data))
206
-
207
- // Update model parameters (this loop remains the same)
208
- for (const item in req.body) {
209
- if (item === "messages" || item === "model" || item === "stream") {
210
- continue
211
- } else if (model_data.parameters && model_data.parameters.hasOwnProperty(item)) { // Check if parameters exist and has the property
212
- model_data.parameters[item] = req.body[item]
213
- }
214
- }
215
- // Ensure the potentially modified model_data (with updated parameters) is in metadata
216
- data.shared_prompt_blueprint.metadata.model = model_data;
217
-
218
- const response = await axios.post(url, data, { headers });
219
- if (response.data.success) {
220
- return response.data.run_group.individual_run_requests[0].id
221
- } else {
222
- return false
223
- }
224
- } catch (error) {
225
- // console.error("错误:", error.response?.data)
226
- res.status(500).json({
227
- "error": {
228
- "message": error.message || "服务器内部错误",
229
- "type": "server_error",
230
- "param": null,
231
- "code": "server_error"
232
- }
233
- })
234
- }
235
- }
236
-
237
- // 聊天完成路由
238
- router.post('/v1/chat/completions', verify, parseMessages, async (req, res) => {
239
- // console.log(JSON.stringify(req.body))
240
-
241
- try {
242
-
243
- const setHeader = () => {
244
- try {
245
- if (req.body.stream === true) {
246
- res.setHeader('Content-Type', 'text/event-stream')
247
- res.setHeader('Cache-Control', 'no-cache')
248
- res.setHeader('Connection', 'keep-alive')
249
- } else {
250
- res.setHeader('Content-Type', 'application/json')
251
- }
252
- } catch (error) {
253
- // console.error("设置响应头时出错:", error)
254
- }
255
- }
256
-
257
- const { access_token, clientId } = req.account
258
- // 生成会话ID
259
- await getChatID(req, res)
260
-
261
- // 发送的数据
262
- const sendAction = `{"action":10,"channel":"user:${clientId}","params":{"agent":"react-hooks/2.0.2"}}`
263
- // 构建 WebSocket URL
264
- const wsUrl = `wss://realtime.ably.io/?access_token=${encodeURIComponent(access_token)}&clientId=${clientId}&format=json&heartbeats=true&v=3&agent=ably-js%2F2.0.2%20browser`
265
- // 创建 WebSocket 连接
266
- const ws = new WebSocket(wsUrl)
267
-
268
- // 状态详细
269
- let ThinkingLastContent = ""
270
- let TextLastContent = ""
271
- let ThinkingStart = false
272
- let ThinkingEnd = false
273
- let RequestID = ""
274
- let MessageID = "chatcmpl-" + uuidv4()
275
- let streamChunk = {
276
- "id": MessageID,
277
- "object": "chat.completion.chunk",
278
- "system_fingerprint": "fp_44709d6fcb",
279
- "created": Math.floor(Date.now() / 1000),
280
- "model": req.body.model,
281
- "choices": [
282
- {
283
- "index": 0,
284
- "delta": {
285
- "content": null
286
- },
287
- "finish_reason": null
288
- }
289
- ]
290
- }
291
-
292
- let pingInterval;
293
-
294
- ws.on('open', async () => {
295
- ws.send(sendAction)
296
- RequestID = await sentRequest(req, res)
297
- setHeader()
298
- // Start sending pings every 30 seconds to keep the connection alive
299
- pingInterval = setInterval(() => {
300
- if (ws.readyState === WebSocket.OPEN) {
301
- ws.ping(() => {}); // Empty callback, just to send the ping
302
- }
303
- }, 15000);
304
- })
305
-
306
- ws.on('message', async (data) => {
307
- try {
308
- data = data.toString()
309
- console.log("here!!!")
310
- console.log(data)
311
- let ContentText = JSON.parse(data)?.messages?.[0]
312
- let ContentData = JSON.parse(ContentText?.data)
313
- const isRequestID = ContentData?.individual_run_request_id
314
- if (isRequestID != RequestID || !isRequestID) return
315
-
316
- let output = ""
317
-
318
- if (ContentText?.name === "UPDATE_LAST_MESSAGE") {
319
- const MessageArray = ContentData?.payload?.message?.content
320
- for (const item of MessageArray) {
321
-
322
- if (item.type === "text") {
323
- output = item.text.replace(TextLastContent, "")
324
- if (ThinkingStart && !ThinkingEnd) {
325
- ThinkingEnd = true
326
- output = `${output}\n\n</think>`
327
- }
328
- TextLastContent = item.text
329
- }
330
- else if (item.type === "thinking" && MessageArray.length === 1) {
331
- output = item.thinking.replace(ThinkingLastContent, "")
332
- if (!ThinkingStart) {
333
- ThinkingStart = true
334
- output = `<think>\n\n${output}`
335
- }
336
- ThinkingLastContent = item.thinking
337
- }
338
-
339
- }
340
-
341
- if (req.body.stream === true) {
342
- streamChunk.choices[0].delta.content = output
343
- res.write(`data: ${JSON.stringify(streamChunk)}\n\n`)
344
- }
345
-
346
- }
347
- else if (ContentText?.name === "INDIVIDUAL_RUN_COMPLETE") {
348
-
349
- if (req.body.stream !== true) {
350
- output = ThinkingLastContent ? `<think>\n\n${ThinkingLastContent}\n\n</think>\n\n${TextLastContent}` : TextLastContent
351
- }
352
-
353
- if (ThinkingLastContent === "" && TextLastContent === "") {
354
- output = "该模型在发送请求时遇到错误: \n1. 请检查请求参数,模型支持参数和默认参数可在/v1/models下查看\n2. 参数设置大小是否超过模型限制\n3. 模型当前官网此模型可能负载过高,可以切换别的模型尝试,这属于正常现象\n4. Anthropic系列模型的temperature的取值为0-1,请勿设置超过1的值\n5. 交流与支持群: https://t.me/nodejs_project"
355
- streamChunk.choices[0].delta.content = output
356
- res.write(`data: ${JSON.stringify(streamChunk)}\n\n`)
357
- }
358
-
359
- if (!req.body.stream || req.body.stream !== true) {
360
- let responseJson = {
361
- "id": MessageID,
362
- "object": "chat.completion",
363
- "created": Math.floor(Date.now() / 1000),
364
- "system_fingerprint": "fp_44709d6fcb",
365
- "model": req.body.model,
366
- "choices": [
367
- {
368
- "index": 0,
369
- "message": {
370
- "role": "assistant",
371
- "content": output
372
- },
373
- "finish_reason": "stop"
374
- }
375
- ],
376
- "usage": {
377
- "prompt_tokens": 0,
378
- "completion_tokens": 0,
379
- "total_tokens": 0
380
- }
381
- }
382
-
383
- res.json(responseJson)
384
- ws.close()
385
- return
386
- } else {
387
- // 流式响应:发送结束标记
388
- let finalChunk = {
389
- "id": MessageID,
390
- "object": "chat.completion.chunk",
391
- "system_fingerprint": "fp_44709d6fcb",
392
- "created": Math.floor(Date.now() / 1000),
393
- "model": req.body.model,
394
- "choices": [
395
- {
396
- "index": 0,
397
- "delta": {},
398
- "finish_reason": "stop"
399
- }
400
- ]
401
- }
402
-
403
- res.write(`data: ${JSON.stringify(finalChunk)}\n\n`)
404
- res.write(`data: [DONE]\n\n`)
405
- res.end()
406
- }
407
- ws.close()
408
- }
409
-
410
- } catch (err) {
411
- // console.error("处理WebSocket消息出错:", err)
412
- }
413
- })
414
-
415
- ws.on('error', (err) => {
416
- clearInterval(pingInterval); // Stop sending pings
417
- // 标准OpenAI错误响应格式
418
- res.status(500).json({
419
- "error": {
420
- "message": err.message,
421
- "type": "server_error",
422
- "param": null,
423
- "code": "server_error"
424
- }
425
- })
426
- })
427
-
428
- const oSeriesModels = ["o4-mini", "o4-mini-high", "o3-mini", "o3-mini-high", "o1", "o3", "o3-2025-04-16", "o4-mini-2025-04-16"];
429
- let timeoutDuration = 300 * 1000; // 默认5分钟
430
-
431
- if (oSeriesModels.includes(req.body.model)) {
432
- timeoutDuration = 1200 * 1000; // o系列模型20分钟
433
- }
434
-
435
- const requestTimeout = setTimeout(() => {
436
- clearInterval(pingInterval); // Stop sending pings
437
- if (ws.readyState === WebSocket.OPEN) {
438
- ws.close()
439
- if (!res.headersSent) {
440
- // 标准OpenAI超时错误响应格式
441
- res.status(504).json({
442
- "error": {
443
- "message": "请求超时",
444
- "type": "timeout",
445
- "param": null,
446
- "code": "timeout_error"
447
- }
448
- })
449
- }
450
- }
451
- }, timeoutDuration)
452
-
453
- ws.on('close', () => {
454
- clearInterval(pingInterval); // Stop sending pings
455
- clearTimeout(requestTimeout); // Clear the main request timeout as well if ws closes first
456
- });
457
-
458
- } catch (error) {
459
- console.error("错误:", error)
460
- // 标准OpenAI通用错误响应格式
461
- res.status(500).json({
462
- "error": {
463
- "message": error.message || "服务器内部错误",
464
- "type": "server_error",
465
- "param": null,
466
- "code": "server_error"
467
- }
468
- })
469
- }
470
- })
471
-
472
- module.exports = router
 
 
 
 
 
1
+ const express = require('express')
2
+ const axios = require('axios')
3
+ const WebSocket = require('ws')
4
+ const router = express.Router()
5
+ const { v4: uuidv4 } = require('uuid')
6
+ const { uploadFileBuffer } = require('../lib/upload')
7
+ const verify = require('./verify')
8
+ const modelMap = require('../lib/model-map')
9
+
10
+
11
+ async function parseMessages(req, res, next) {
12
+ const messages = req.body.messages
13
+ if (!Array.isArray(messages)) {
14
+ req.processedMessages = []
15
+ return next()
16
+ }
17
+
18
+ try {
19
+ const transformedMessages = await Promise.all(messages.map(async (msg) => {
20
+ // Determine provider to conditionally convert system role for Anthropic
21
+ const modelName = req.body.model;
22
+ const modelData = modelMap[modelName]; // modelMap is required at the top
23
+ const provider = modelData?.provider; // Use optional chaining for safety
24
+
25
+ let message = {
26
+ role: (msg.role === "system" && provider === "anthropic") ? "user" : msg.role,
27
+ tool_calls: [],
28
+ template_format: "jinja2"
29
+ }
30
+
31
+ if (Array.isArray(msg.content)) {
32
+ const contentItems = await Promise.all(msg.content.map(async (item) => {
33
+ if (item.type === "text") {
34
+ return {
35
+ type: "text",
36
+ text: item.text
37
+ }
38
+ }
39
+ else if (item.type === "image_url") {
40
+ try {
41
+ const base64Match = item.image_url.url.match(/^data:image\/\w+;base64,(.+)$/)
42
+ if (base64Match) {
43
+ const base64 = base64Match[1]
44
+ const data = Buffer.from(base64, 'base64')
45
+ const uploadResult = await uploadFileBuffer(data)
46
+
47
+ return {
48
+ type: "media",
49
+ media: {
50
+ "type": "image",
51
+ "url": uploadResult.file_url,
52
+ "title": `image_${Date.now()}.png`
53
+ }
54
+ }
55
+ } else {
56
+ return {
57
+ type: "media",
58
+ media: {
59
+ "type": "image",
60
+ "url": item.image_url.url,
61
+ "title": "external_image"
62
+ }
63
+ }
64
+ }
65
+ } catch (error) {
66
+ console.error("处理图像时出错:", error)
67
+ return {
68
+ type: "text",
69
+ text: "[图像处理失败]"
70
+ }
71
+ }
72
+ } else {
73
+ return {
74
+ type: "text",
75
+ text: JSON.stringify(item)
76
+ }
77
+ }
78
+ }))
79
+
80
+ message.content = contentItems
81
+ } else {
82
+ message.content = [
83
+ {
84
+ type: "text",
85
+ text: msg.content || ""
86
+ }
87
+ ]
88
+ }
89
+
90
+ return message
91
+ }))
92
+
93
+ req.body.messages = transformedMessages
94
+ return next()
95
+ } catch (error) {
96
+ console.error("处理消息时出错:", error.status)
97
+ req.body.messages = []
98
+ return next(error)
99
+ }
100
+ }
101
+
102
+ async function getChatID(req, res) {
103
+ try {
104
+ const url = 'https://api.promptlayer.com/api/dashboard/v2/workspaces/' + req.account.workspaceId + '/playground_sessions'
105
+ const headers = { Authorization: "Bearer " + req.account.token }
106
+ const model_data = modelMap[req.body.model] ? modelMap[req.body.model] : modelMap["claude-3-7-sonnet-20250219"]
107
+ let data = {
108
+ "id": uuidv4(),
109
+ "name": "Not implemented",
110
+ "prompt_blueprint": {
111
+ "inference_client_name": null,
112
+ "metadata": {
113
+ "model": model_data
114
+ },
115
+ "prompt_template": {
116
+ "type": "chat",
117
+ "messages": req.body.messages,
118
+ "tools": req.body.tools || [],
119
+ "tool_choice": req.body.tool_choice || "none",
120
+ "input_variables": [],
121
+ "functions": [],
122
+ "function_call": null
123
+ },
124
+ "provider_base_url_name": null
125
+ },
126
+ "input_variables": []
127
+ }
128
+
129
+ for (const item in req.body) {
130
+ if (item === "messages" || item === "model" || item === "stream") {
131
+ continue
132
+ } else if (model_data.parameters[item]) {
133
+ model_data.parameters[item] = req.body[item]
134
+ }
135
+ }
136
+ data.prompt_blueprint.metadata.model = model_data
137
+ console.log(`模型参数: ${data.prompt_blueprint.metadata.model}`)
138
+
139
+ const response = await axios.put(url, data, { headers })
140
+ if (response.data.success) {
141
+ console.log(`生成会话ID成功: ${response.data.playground_session.id}`)
142
+ req.chatID = response.data.playground_session.id
143
+ return response.data.playground_session.id
144
+ } else {
145
+ return false
146
+ }
147
+ } catch (error) {
148
+ // console.error("错误:", error.response?.data)
149
+ res.status(500).json({
150
+ "error": {
151
+ "message": error.message || "服务器内部错误",
152
+ "type": "server_error",
153
+ "param": null,
154
+ "code": "server_error"
155
+ }
156
+ })
157
+ return false
158
+ }
159
+ }
160
+
161
+ async function sentRequest(req, res) {
162
+ try {
163
+ const url = 'https://api.promptlayer.com/api/dashboard/v2/workspaces/' + req.account.workspaceId + '/run_groups'
164
+ const headers = { Authorization: "Bearer " + req.account.token }
165
+ const model_data = modelMap[req.body.model] ? modelMap[req.body.model] : modelMap["claude-3-7-sonnet-20250219"];
166
+ const provider = model_data?.provider; // Get provider
167
+
168
+ // Base prompt template structure
169
+ let prompt_template = {
170
+ "type": "chat",
171
+ "messages": req.body.messages,
172
+ "tools": req.body.tools || [], // Default value
173
+ "tool_choice": req.body.tool_choice || "none", // Default value
174
+ "input_variables": [],
175
+ "functions": [],
176
+ "function_call": null
177
+ };
178
+
179
+ // Conditionally modify for Mistral/Cohere
180
+ if (provider === 'mistral' || provider === 'cohere') {
181
+ prompt_template.tools = null;
182
+ delete prompt_template.tool_choice; // Remove tool_choice entirely
183
+ delete prompt_template.function_call;
184
+ }
185
+
186
+ let data = {
187
+ "id": uuidv4(),
188
+ "playground_session_id": req.chatID,
189
+ "shared_prompt_blueprint": {
190
+ "inference_client_name": null,
191
+ "metadata": {
192
+ "model": model_data // Keep original model_data here for metadata
193
+ },
194
+ "prompt_template": prompt_template, // Use the adjusted template
195
+ "provider_base_url_name": null
196
+ },
197
+ "individual_run_requests": [
198
+ {
199
+ "input_variables": {},
200
+ "run_group_position": 1
201
+ }
202
+ ]
203
+ };
204
+
205
+ console.log(JSON.stringify(data))
206
+
207
+ // Update model parameters (this loop remains the same)
208
+ for (const item in req.body) {
209
+ if (item === "messages" || item === "model" || item === "stream") {
210
+ continue
211
+ } else if (model_data.parameters && model_data.parameters.hasOwnProperty(item)) { // Check if parameters exist and has the property
212
+ model_data.parameters[item] = req.body[item]
213
+ }
214
+ }
215
+ // Ensure the potentially modified model_data (with updated parameters) is in metadata
216
+ data.shared_prompt_blueprint.metadata.model = model_data;
217
+
218
+ const response = await axios.post(url, data, { headers });
219
+ if (response.data.success) {
220
+ return response.data.run_group.individual_run_requests[0].id
221
+ } else {
222
+ return false
223
+ }
224
+ } catch (error) {
225
+ // console.error("错误:", error.response?.data)
226
+ res.status(500).json({
227
+ "error": {
228
+ "message": error.message || "服务器内部错误",
229
+ "type": "server_error",
230
+ "param": null,
231
+ "code": "server_error"
232
+ }
233
+ })
234
+ }
235
+ }
236
+
237
+ // 聊天完成路由
238
+ router.post('/v1/chat/completions', verify, parseMessages, async (req, res) => {
239
+ // console.log(JSON.stringify(req.body))
240
+
241
+ try {
242
+
243
+ const setHeader = () => {
244
+ try {
245
+ if (req.body.stream === true) {
246
+ res.setHeader('Content-Type', 'text/event-stream')
247
+ res.setHeader('Cache-Control', 'no-cache')
248
+ res.setHeader('Connection', 'keep-alive')
249
+ } else {
250
+ res.setHeader('Content-Type', 'application/json')
251
+ }
252
+ } catch (error) {
253
+ // console.error("设置响应头时出错:", error)
254
+ }
255
+ }
256
+
257
+ const { access_token, clientId } = req.account
258
+ // 生成会话ID
259
+ await getChatID(req, res)
260
+
261
+ // 发送的数据
262
+ const sendAction = `{"action":10,"channel":"user:${clientId}","params":{"agent":"react-hooks/2.0.2"}}`
263
+ // 构建 WebSocket URL
264
+ const wsUrl = `wss://realtime.ably.io/?access_token=${encodeURIComponent(access_token)}&clientId=${clientId}&format=json&heartbeats=true&v=3&agent=ably-js%2F2.0.2%20browser`
265
+ // 创建 WebSocket 连接
266
+ const ws = new WebSocket(wsUrl)
267
+
268
+ // 状态详细
269
+ let ThinkingLastContent = ""
270
+ let TextLastContent = ""
271
+ let ThinkingStart = false
272
+ let ThinkingEnd = false
273
+ let RequestID = ""
274
+ let MessageID = "chatcmpl-" + uuidv4()
275
+ let streamChunk = {
276
+ "id": MessageID,
277
+ "object": "chat.completion.chunk",
278
+ "system_fingerprint": "fp_44709d6fcb",
279
+ "created": Math.floor(Date.now() / 1000),
280
+ "model": req.body.model,
281
+ "choices": [
282
+ {
283
+ "index": 0,
284
+ "delta": {
285
+ "content": null
286
+ },
287
+ "finish_reason": null
288
+ }
289
+ ]
290
+ }
291
+
292
+ let pingInterval;
293
+
294
+ ws.on('open', async () => {
295
+ ws.send(sendAction)
296
+ RequestID = await sentRequest(req, res)
297
+ setHeader()
298
+ // Start sending pings every 30 seconds to keep the connection alive
299
+ pingInterval = setInterval(() => {
300
+ if (ws.readyState === WebSocket.OPEN) {
301
+ ws.ping(() => {}); // Empty callback, just to send the ping
302
+ }
303
+ }, 15000);
304
+ })
305
+
306
+ ws.on('message', async (data) => {
307
+ try {
308
+ data = data.toString()
309
+ console.log("here!!!")
310
+ console.log(data)
311
+ let ContentText = JSON.parse(data)?.messages?.[0]
312
+ let ContentData = JSON.parse(ContentText?.data)
313
+ const isRequestID = ContentData?.individual_run_request_id
314
+ if (isRequestID != RequestID || !isRequestID) return
315
+
316
+ let output = ""
317
+
318
+ if (ContentText?.name === "UPDATE_LAST_MESSAGE") {
319
+ const MessageArray = ContentData?.payload?.message?.content
320
+ for (const item of MessageArray) {
321
+
322
+ if (item.type === "text") {
323
+ output = item.text.replace(TextLastContent, "")
324
+ if (ThinkingStart && !ThinkingEnd) {
325
+ ThinkingEnd = true
326
+ output = `${output}\n\n</think>`
327
+ }
328
+ TextLastContent = item.text
329
+ }
330
+ else if (item.type === "thinking" && MessageArray.length === 1) {
331
+ output = item.thinking.replace(ThinkingLastContent, "")
332
+ if (!ThinkingStart) {
333
+ ThinkingStart = true
334
+ output = `<think>\n\n${output}`
335
+ }
336
+ ThinkingLastContent = item.thinking
337
+ }
338
+
339
+ }
340
+
341
+ if (req.body.stream === true) {
342
+ streamChunk.choices[0].delta.content = output
343
+ res.write(`data: ${JSON.stringify(streamChunk)}\n\n`)
344
+ }
345
+
346
+ }
347
+ else if (ContentText?.name === "INDIVIDUAL_RUN_COMPLETE") {
348
+
349
+ if (req.body.stream !== true) {
350
+ output = ThinkingLastContent ? `<think>\n\n${ThinkingLastContent}\n\n</think>\n\n${TextLastContent}` : TextLastContent
351
+ }
352
+
353
+ if (ThinkingLastContent === "" && TextLastContent === "") {
354
+ const modelName = req.body.model;
355
+ const modelData = modelMap[modelName] || modelMap["claude-3-7-sonnet-20250219"]; // Fallback to default if model not found
356
+ const provider = modelData.provider || "anthropic"; // Default to anthropic if provider not found
357
+ const providerUpperCase = provider.charAt(0).toUpperCase() + provider.slice(1);
358
+ output = `${provider}.BadRequestError: Error code: 400 - {'type': 'error', 'error': {'type': 'invalid_request_error', 'message': 'Your credit balance is too low to access the ${providerUpperCase} API. Please go to Plans & Billing to upgrade or purchase credits.'}}`
359
+ streamChunk.choices[0].delta.content = output
360
+ res.write(`data: ${JSON.stringify(streamChunk)}\n\n`)
361
+ }
362
+
363
+ if (!req.body.stream || req.body.stream !== true) {
364
+ let responseJson = {
365
+ "id": MessageID,
366
+ "object": "chat.completion",
367
+ "created": Math.floor(Date.now() / 1000),
368
+ "system_fingerprint": "fp_44709d6fcb",
369
+ "model": req.body.model,
370
+ "choices": [
371
+ {
372
+ "index": 0,
373
+ "message": {
374
+ "role": "assistant",
375
+ "content": output
376
+ },
377
+ "finish_reason": "stop"
378
+ }
379
+ ],
380
+ "usage": {
381
+ "prompt_tokens": 0,
382
+ "completion_tokens": 0,
383
+ "total_tokens": 0
384
+ }
385
+ }
386
+
387
+ res.json(responseJson)
388
+ ws.close()
389
+ return
390
+ } else {
391
+ // 流式响应:发送结束标记
392
+ let finalChunk = {
393
+ "id": MessageID,
394
+ "object": "chat.completion.chunk",
395
+ "system_fingerprint": "fp_44709d6fcb",
396
+ "created": Math.floor(Date.now() / 1000),
397
+ "model": req.body.model,
398
+ "choices": [
399
+ {
400
+ "index": 0,
401
+ "delta": {},
402
+ "finish_reason": "stop"
403
+ }
404
+ ]
405
+ }
406
+
407
+ res.write(`data: ${JSON.stringify(finalChunk)}\n\n`)
408
+ res.write(`data: [DONE]\n\n`)
409
+ res.end()
410
+ }
411
+ ws.close()
412
+ }
413
+
414
+ } catch (err) {
415
+ // console.error("处理WebSocket消息出错:", err)
416
+ }
417
+ })
418
+
419
+ ws.on('error', (err) => {
420
+ clearInterval(pingInterval); // Stop sending pings
421
+ // 标准OpenAI错误响应格式
422
+ res.status(500).json({
423
+ "error": {
424
+ "message": err.message,
425
+ "type": "server_error",
426
+ "param": null,
427
+ "code": "server_error"
428
+ }
429
+ })
430
+ })
431
+
432
+ const oSeriesModels = ["o4-mini", "o4-mini-high", "o3-mini", "o3-mini-high", "o1", "o3", "o3-2025-04-16", "o4-mini-2025-04-16"];
433
+ let timeoutDuration = 300 * 1000; // 默认5分钟
434
+
435
+ if (oSeriesModels.includes(req.body.model)) {
436
+ timeoutDuration = 1200 * 1000; // o系列模型20分钟
437
+ }
438
+
439
+ const requestTimeout = setTimeout(() => {
440
+ clearInterval(pingInterval); // Stop sending pings
441
+ if (ws.readyState === WebSocket.OPEN) {
442
+ ws.close()
443
+ if (!res.headersSent) {
444
+ // 标准OpenAI超时错误响应格式
445
+ res.status(504).json({
446
+ "error": {
447
+ "message": "请求超时",
448
+ "type": "timeout",
449
+ "param": null,
450
+ "code": "timeout_error"
451
+ }
452
+ })
453
+ }
454
+ }
455
+ }, timeoutDuration)
456
+
457
+ ws.on('close', () => {
458
+ clearInterval(pingInterval); // Stop sending pings
459
+ clearTimeout(requestTimeout); // Clear the main request timeout as well if ws closes first
460
+ });
461
+
462
+ } catch (error) {
463
+ console.error("错误:", error)
464
+ // 标准OpenAI通用错误响应格式
465
+ res.status(500).json({
466
+ "error": {
467
+ "message": error.message || "服务器内部错误",
468
+ "type": "server_error",
469
+ "param": null,
470
+ "code": "server_error"
471
+ }
472
+ })
473
+ }
474
+ })
475
+
476
+ module.exports = router