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

Upload 9 files

Browse files
Files changed (2) hide show
  1. src/lib/model-map.js +3 -3
  2. src/routes/chat.js +476 -476
src/lib/model-map.js CHANGED
@@ -7,7 +7,7 @@ const modelMap = {
7
  "max_tokens": 64000,
8
  "temperature": 1,
9
  "top_k": 0,
10
- "top_p": 0
11
  }
12
  },
13
  "claude-3-7-sonnet-20250219-thinking": {
@@ -30,7 +30,7 @@ const modelMap = {
30
  "max_tokens": 64000,
31
  "temperature": 1,
32
  "top_k": 0,
33
- "top_p": 0
34
  }
35
  },
36
  "claude-sonnet-4-20250514-thinking": {
@@ -53,7 +53,7 @@ const modelMap = {
53
  "max_tokens": 32000,
54
  "temperature": 1,
55
  "top_k": 0,
56
- "top_p": 0
57
  }
58
  },
59
  "claude-opus-4-20250514-thinking": {
 
7
  "max_tokens": 64000,
8
  "temperature": 1,
9
  "top_k": 0,
10
+ "top_p": 1
11
  }
12
  },
13
  "claude-3-7-sonnet-20250219-thinking": {
 
30
  "max_tokens": 64000,
31
  "temperature": 1,
32
  "top_k": 0,
33
+ "top_p": 1
34
  }
35
  },
36
  "claude-sonnet-4-20250514-thinking": {
 
53
  "max_tokens": 32000,
54
  "temperature": 1,
55
  "top_k": 0,
56
+ "top_p": 1
57
  }
58
  },
59
  "claude-opus-4-20250514-thinking": {
src/routes/chat.js CHANGED
@@ -1,476 +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
- 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
 
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