hongshi-files commited on
Commit
351e107
·
verified ·
1 Parent(s): 67cf308

Update main.ts

Browse files
Files changed (1) hide show
  1. main.ts +909 -415
main.ts CHANGED
@@ -1,431 +1,925 @@
1
- import { serve } from "https://deno.land/std@0.220.1/http/server.ts";
2
-
3
- // 定义常量
4
- const API_URL = "https://mcp.scira.ai/api/chat";
5
- const FIXED_USER_ID = "2jFMDM1A1R_XxOTxPjhwe";
6
- const FIXED_CHAT_ID = "ZIWa36kd6MSqzw-ifXGzE";
7
- const DEFAULT_MODEL = "qwen-qwq";
8
- const PORT = 7860;
9
-
10
- // 定义接口
11
- interface Message {
12
- role: string;
13
- content: string;
14
- parts?: Array<{
15
- type: string;
16
- text: string;
17
- }>;
18
- }
19
-
20
- interface SciraPayload {
21
- id: string;
22
- messages: Message[];
23
- selectedModel: string;
24
- mcpServers: any[];
25
- chatId: string;
26
- userId: string;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  }
28
 
29
  interface OpenAIModel {
30
- id: string;
31
- created: number;
32
- object: string;
33
- }
34
-
35
- // 可用模型列表
36
- const AVAILABLE_MODELS: OpenAIModel[] = [
37
- {
38
- id: "qwen-qwq",
39
- created: Date.now(),
40
- object: "model",
41
- },
42
- {
43
- id: "gemini-2.5-flash",
44
- created: Date.now(),
45
- object: "model",
46
- },
47
- {
48
- id: "gpt-4.1-mini",
49
- created: Date.now(),
50
- object: "model",
51
- },
52
- {
53
- id: "claude-3-7-sonnet",
54
- created: Date.now(),
55
- object: "model",
56
- },
57
- ];
58
-
59
- // 格式化消息为Scira格式
60
- function formatMessagesForScira(messages: Message[]): Message[] {
61
- return messages.map(msg => ({
62
- role: msg.role,
63
- content: msg.content,
64
- parts: [{
65
- type: "text",
66
- text: msg.content
67
- }]
68
- }));
69
- }
70
-
71
- // 构建Scira请求负载
72
- function buildSciraPayload(messages: Message[], model = DEFAULT_MODEL): SciraPayload {
73
- const formattedMessages = formatMessagesForScira(messages);
74
- return {
75
- id: FIXED_CHAT_ID,
76
- messages: formattedMessages,
77
- selectedModel: model,
78
- mcpServers: [],
79
- chatId: FIXED_CHAT_ID,
80
- userId: FIXED_USER_ID
81
- };
82
- }
83
-
84
- // 处理模型列表请求
85
- async function handleModelsRequest(): Promise<Response> {
86
- const response = {
87
- object: "list",
88
- data: AVAILABLE_MODELS,
89
- };
90
- return new Response(JSON.stringify(response), {
91
- headers: {
92
- "Content-Type": "application/json",
93
- "Access-Control-Allow-Origin": "*"
94
- },
95
- });
96
- }
97
-
98
- // 处理聊天补全请求
99
- async function handleChatCompletionsRequest(req: Request): Promise<Response> {
100
- const requestData = await req.json();
101
- const { messages, model = DEFAULT_MODEL, stream = false } = requestData;
102
-
103
- const sciraPayload = buildSciraPayload(messages, model);
104
- const response = await fetch(API_URL, {
105
- method: "POST",
106
- headers: {
107
- "Content-Type": "application/json",
108
- "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:137.0) Gecko/20100101 Firefox/137.0",
109
- "Accept": "*/*",
110
- "Referer": `https://mcp.scira.ai/chat/${FIXED_CHAT_ID}`,
111
- "Origin": "https://mcp.scira.ai",
112
- },
113
- body: JSON.stringify(sciraPayload),
114
- });
115
-
116
- if (stream) {
117
- return handleStreamResponse(response, model);
118
- } else {
119
- return handleRegularResponse(response, model);
120
- }
121
- }
122
-
123
- // 处理流式响应
124
- async function handleStreamResponse(response: Response, model: string): Promise<Response> {
125
- const reader = response.body!.getReader();
126
- const encoder = new TextEncoder();
127
- const decoder = new TextDecoder();
128
-
129
- const id = `chatcmpl-${Date.now().toString(36)}${Math.random().toString(36).substring(2, 10)}`;
130
- const createdTime = Math.floor(Date.now() / 1000);
131
- const systemFingerprint = `fp_${Math.random().toString(36).substring(2, 12)}`;
132
-
133
- const stream = new ReadableStream({
134
- async start(controller) {
135
- // 发送流式头部
136
- const headerEvent = {
137
- id: id,
138
- object: "chat.completion.chunk",
139
- created: createdTime,
140
- model: model,
141
- system_fingerprint: systemFingerprint,
142
- choices: [{
143
- index: 0,
144
- delta: { role: "assistant" },
145
- logprobs: null,
146
- finish_reason: null
147
- }]
148
- };
149
- controller.enqueue(encoder.encode(`data: ${JSON.stringify(headerEvent)}\n\n`));
150
-
151
- try {
152
- let buffer = "";
153
-
154
  while (true) {
155
- const { done, value } = await reader.read();
156
- if (done) break;
157
-
158
- // 解码当前数据块并添加到缓冲区
159
- buffer += decoder.decode(value, { stream: true });
160
-
161
- // 处理完整的行
162
- const lines = buffer.split('\n');
163
- // 保留最后一个可能不完整的行
164
- buffer = lines.pop() || "";
165
-
166
- // 处理并立即发送每一行
167
- for (const line of lines) {
168
- if (!line.trim()) continue;
169
-
170
- if (line.startsWith('g:')) {
171
- // 对于g开头的行,输出reasoning_content
172
- let content = line.slice(2).replace(/^"/, "").replace(/"$/, "");
173
- content = content.replace(/\\n/g, "\n");
174
-
175
- const event = {
176
- id: id,
177
- object: "chat.completion.chunk",
178
- created: createdTime,
179
- model: model,
180
- system_fingerprint: systemFingerprint,
181
- choices: [{
182
- index: 0,
183
- delta: { reasoning_content: content },
184
- logprobs: null,
185
- finish_reason: null
186
- }]
187
- };
188
- controller.enqueue(encoder.encode(`data: ${JSON.stringify(event)}\n\n`));
189
- } else if (line.startsWith('0:')) {
190
- // 对于0开头的行,输出content
191
- let content = line.slice(2).replace(/^"/, "").replace(/"$/, "");
192
- content = content.replace(/\\n/g, "\n");
193
-
194
- const event = {
195
- id: id,
196
- object: "chat.completion.chunk",
197
- created: createdTime,
198
- model: model,
199
- system_fingerprint: systemFingerprint,
200
- choices: [{
201
- index: 0,
202
- delta: { content: content },
203
- logprobs: null,
204
- finish_reason: null
205
- }]
206
- };
207
- controller.enqueue(encoder.encode(`data: ${JSON.stringify(event)}\n\n`));
208
- } else if (line.startsWith('e:')) {
209
- // 完成消息
210
- try {
211
- const finishData = JSON.parse(line.slice(2));
212
- const event = {
213
- id: id,
214
- object: "chat.completion.chunk",
215
- created: createdTime,
216
- model: model,
217
- system_fingerprint: systemFingerprint,
218
- choices: [{
219
- index: 0,
220
- delta: {},
221
- logprobs: null,
222
- finish_reason: finishData.finishReason || "stop"
223
- }]
224
- };
225
- controller.enqueue(encoder.encode(`data: ${JSON.stringify(event)}\n\n`));
226
- } catch (error) {
227
- console.error("Error parsing finish data:", error);
228
- }
229
  }
230
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
231
  }
232
-
233
- // 处理缓冲区中剩余的内容(如果有的话)
234
- if (buffer.trim()) {
235
- const line = buffer.trim();
236
- if (line.startsWith('g:')) {
237
- let content = line.slice(2).replace(/^"/, "").replace(/"$/, "");
238
- content = content.replace(/\\n/g, "\n");
239
-
240
- const event = {
241
- id: id,
242
- object: "chat.completion.chunk",
243
- created: createdTime,
244
- model: model,
245
- system_fingerprint: systemFingerprint,
246
- choices: [{
247
- index: 0,
248
- delta: { reasoning_content: content },
249
- logprobs: null,
250
- finish_reason: null
251
- }]
252
- };
253
- controller.enqueue(encoder.encode(`data: ${JSON.stringify(event)}\n\n`));
254
- } else if (line.startsWith('0:')) {
255
- let content = line.slice(2).replace(/^"/, "").replace(/"$/, "");
256
- content = content.replace(/\\n/g, "\n");
257
-
258
- const event = {
259
- id: id,
260
- object: "chat.completion.chunk",
261
- created: createdTime,
262
- model: model,
263
- system_fingerprint: systemFingerprint,
264
- choices: [{
265
- index: 0,
266
- delta: { content: content },
267
- logprobs: null,
268
- finish_reason: null
269
- }]
270
- };
271
- controller.enqueue(encoder.encode(`data: ${JSON.stringify(event)}\n\n`));
272
- }
273
  }
274
- } catch (error) {
275
- console.error("Stream error:", error);
276
- } finally {
277
- // 确保发送 "data: [DONE]"
278
- controller.enqueue(encoder.encode("data: [DONE]\n\n"));
279
- controller.close();
280
- }
281
  }
282
- });
283
-
284
- return new Response(stream, {
285
- headers: {
286
- "Content-Type": "text/event-stream",
287
- "Cache-Control": "no-cache",
288
- "Connection": "keep-alive",
289
- "Access-Control-Allow-Origin": "*",
290
- },
291
- });
292
- }
293
-
294
- // 处理非流式响应
295
- async function handleRegularResponse(response: Response, model: string): Promise<Response> {
296
- const text = await response.text();
297
- const lines = text.split('\n');
298
-
299
- let content = "";
300
- let reasoning_content = "";
301
- let usage = { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 };
302
- let finish_reason = "stop";
303
-
304
- for (const line of lines) {
305
- if (!line.trim()) continue;
306
-
307
- if (line.startsWith('0:')) {
308
- // 常规内容 - 处理转义的换行符
309
- let lineContent = line.slice(2).replace(/^"/, "").replace(/"$/, "");
310
- lineContent = lineContent.replace(/\\n/g, "\n");
311
- content += lineContent;
312
- } else if (line.startsWith('g:')) {
313
- // 推理内容 - 处理转义的换行符
314
- let lineContent = line.slice(2).replace(/^"/, "").replace(/"$/, "");
315
- lineContent = lineContent.replace(/\\n/g, "\n");
316
- reasoning_content += lineContent;
317
- } else if (line.startsWith('e:')) {
318
- try {
319
- const finishData = JSON.parse(line.slice(2));
320
- if (finishData.finishReason) {
321
- finish_reason = finishData.finishReason;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
322
  }
323
- } catch (error) {
324
- console.error("Error parsing finish data:", error);
325
- }
326
- } else if (line.startsWith('d:')) {
327
- try {
328
- const finishData = JSON.parse(line.slice(2));
329
- if (finishData.usage) {
330
- usage.prompt_tokens = finishData.usage.promptTokens || 0;
331
- usage.completion_tokens = finishData.usage.completionTokens || 0;
332
- usage.total_tokens = usage.prompt_tokens + usage.completion_tokens;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
333
  }
334
- } catch (error) {
335
- console.error("Error parsing usage data:", error);
336
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
337
  }
338
- }
339
-
340
- const systemFingerprint = `fp_${Math.random().toString(36).substring(2, 12)}`;
341
- const id = `chatcmpl-${Date.now().toString(36)}${Math.random().toString(36).substring(2, 10)}`;
342
-
343
- const openAIResponse = {
344
- id: id,
345
- object: "chat.completion",
346
- created: Math.floor(Date.now() / 1000),
347
- model: model,
348
- system_fingerprint: systemFingerprint,
349
- choices: [{
350
- index: 0,
351
- message: {
352
- role: "assistant",
353
- content: content
354
- },
355
- logprobs: null,
356
- finish_reason: finish_reason
357
- }],
358
- usage: usage
359
- };
360
-
361
- // 如果存在推理内容,添加到消息中
362
- if (reasoning_content.trim()) {
363
- openAIResponse.choices[0].message.reasoning_content = reasoning_content;
364
- }
365
-
366
- return new Response(JSON.stringify(openAIResponse), {
367
- headers: {
368
- "Content-Type": "application/json",
369
- "Access-Control-Allow-Origin": "*"
370
  },
371
- });
372
- }
373
-
374
- // 主请求处理函数
375
- async function handler(req: Request): Promise<Response> {
376
- const url = new URL(req.url);
377
-
378
- // 设置CORS头
379
- const headers = {
380
- "Access-Control-Allow-Origin": "*",
381
- "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
382
- "Access-Control-Allow-Headers": "Content-Type, Authorization",
383
- };
384
-
385
- // 处理OPTIONS请求(CORS预检)
386
- if (req.method === "OPTIONS") {
387
- return new Response(null, {
388
- headers,
389
- status: 204
390
- });
391
- }
392
-
393
- try {
394
- // 处理模型列表接口
395
- if (url.pathname === "/v1/models") {
396
- return handleModelsRequest();
397
  }
398
-
399
- // 处理聊天补全接口
400
- if (url.pathname === "/v1/chat/completions") {
401
- return handleChatCompletionsRequest(req);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
402
  }
403
-
404
- // 未找到的路由
405
- return new Response(
406
- JSON.stringify({ error: "Not found" }), {
407
- status: 404,
408
- headers: {
409
- "Content-Type": "application/json",
410
- ...headers
411
- },
412
- }
413
- );
414
- } catch (error) {
415
- console.error("Error processing request:", error);
416
- return new Response(
417
- JSON.stringify({ error: error.message || "Internal server error" }),
418
- {
419
- status: 500,
420
- headers: {
421
- "Content-Type": "application/json",
422
- ...headers
423
- },
424
- }
425
- );
426
- }
427
  }
428
 
429
- // 启动服务器
430
- console.log(`Starting server on port ${PORT}...`);
431
- serve(handler, { port: PORT });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ Copyright (c) 2025 Neuroplexus
3
+
4
+ This software is provided "as is", without warranty of any kind, express or
5
+ implied, including but not limited to the warranties of merchantability,
6
+ fitness for a particular purpose and noninfringement. In no event shall the
7
+ authors or copyright holders be liable for any claim, damages or other
8
+ liability, whether in an action of contract, tort or otherwise, arising from,
9
+ out of or in connection with the software or the use or other dealings in the
10
+ software.
11
+
12
+ This software is licensed under the Neuroplexus Non-Commercial Share-Alike License (the "License");
13
+ you may not use this file except in compliance with the License.
14
+
15
+ Terms and Conditions:
16
+
17
+ 1. Non-Commercial Use Only:
18
+
19
+ This software and its associated documentation (collectively, the "Software")
20
+ are strictly prohibited from being used, directly or indirectly, for any
21
+ commercial purpose. "Commercial purpose" includes, but is not limited to:
22
+ * Use in a product or service offered for sale or other consideration.
23
+ * Use in a product or service that provides a competitive advantage in a
24
+ commercial setting.
25
+ * Use in internal business operations that generate revenue or provide
26
+ cost savings directly attributable to the Software.
27
+ * Use in training or educational programs for which a fee is charged.
28
+ * Use to support any for-profit entity, regardless of whether the software
29
+ itself is sold.
30
+ * Reselling, or sublicensing this software.
31
+ If you require a commercial license, please contact Neuroplexus at isneuroplexus@duck.com.
32
+
33
+ 2. Attribution and Original Author/Project Notice:
34
+
35
+ Any use, distribution, or modification of the Software (in whole or in part)
36
+ must prominently include the following:
37
+ * The original copyright notice: `Copyright (c) 2025 Neuroplexus`
38
+ * A clear and unambiguous statement identifying Neuroplexus as the original
39
+ author of the Software.
40
+ * A link or reference to the original project location (e.g., a URL to a
41
+ repository, if applicable). For example: "Based on the Neuroplexus
42
+ HuanyuanInterface project, available at https://linux.do/t/topic/507324.
43
+
44
+ 3. Share-Alike (Derivative Works):
45
+
46
+ If you modify the Software, any distribution of the modified version (the
47
+ "Derivative Work") must be licensed under the *same* terms and conditions as
48
+ this License (Neuroplexus Non-Commercial Share-Alike License). This means:
49
+ * The Derivative Work must also be restricted to non-commercial use.
50
+ * The Derivative Work must include the attribution requirements outlined
51
+ in Section 2.
52
+ * The source code of the Derivative Work must be made available under
53
+ this same License.
54
+
55
+ 4. Modification Notices:
56
+
57
+ Any Derivative Work must include prominent notices stating that you have
58
+ modified the Software, and the date and nature of the changes made. These
59
+ notices must be placed:
60
+ * In the source code files that have been modified.
61
+ * In a separate `CHANGELOG` or `MODIFICATIONS` file included with the
62
+ Derivative Work's distribution. This file should clearly list all
63
+ modifications made to the original Software.
64
+
65
+ 5. No Endorsement:
66
+
67
+ The names of Neuroplexus or its contributors may not be used to endorse or
68
+ promote products derived from this Software without specific prior written
69
+ permission.
70
+
71
+ 6. Termination:
72
+
73
+ This License automatically terminates if you violate any of its terms and
74
+ conditions. Upon termination, you must cease all use, distribution, and
75
+ modification of the Software and destroy all copies in your possession.
76
+
77
+ 7. Severability:
78
+
79
+ If any provision of this License is held to be invalid or unenforceable, the
80
+ remaining provisions shall remain in full force and effect.
81
+
82
+ 8. Governing Law:
83
+
84
+ This License shall be governed by and construed in accordance with the laws
85
+ of New South Wales, Australia, without
86
+ regard to its conflict of law principles.
87
+
88
+ 9. Entire Agreement:
89
+
90
+ This license constitutes the entire agreement with respect to the software.
91
+ Neuroplexus is not bound by any additional provisions that may appear in any
92
+ communication from you.
93
+ */
94
+
95
+ import { Application, Router, Context } from "https://deno.land/x/oak@v12.6.1/mod.ts";
96
+ import { Buffer } from "https://deno.land/std@0.152.0/io/buffer.ts"; // Not used, can be removed
97
+
98
+ const HUNYUAN_API_URL = "http://llm.hunyuan.tencent.com/aide/api/v2/triton_image/demo_text_chat/"; // Consider making this configurable
99
+ const DEFAULT_STAFFNAME = "staryxzhang"; // Consider making this configurable
100
+ const DEFAULT_WSID = "10697"; // Consider making this configurable
101
+ const API_KEY = "7auGXNATFSKl7dc"; // Consider loading this from an environment variable or config file
102
+
103
+ interface HunyuanMessage {
104
+ role: string;
105
+ content: string;
106
+ reasoning_content?: string;
107
+ }
108
+
109
+ interface HunyuanRequest {
110
+ stream: boolean;
111
+ model: string;
112
+ query_id: string;
113
+ messages: HunyuanMessage[];
114
+ stream_moderation: boolean;
115
+ enable_enhancement: boolean;
116
+ }
117
+
118
+ // These interfaces can be combined for better readability
119
+ interface OpenAIChoiceBase {
120
+ index: number;
121
+ finish_reason: string | null;
122
+ }
123
+
124
+ interface OpenAIChoiceDelta extends OpenAIChoiceBase {
125
+ delta: {
126
+ role?: string;
127
+ content?: string;
128
+ reasoning_content?: string;
129
+ };
130
+ }
131
+
132
+ interface OpenAIChoiceNonStream extends OpenAIChoiceBase {
133
+ message: {
134
+ role: string;
135
+ content: string;
136
+ reasoning_content?: string;
137
+ };
138
+ }
139
+
140
+ interface OpenAIStreamResponse {
141
+ id: string;
142
+ object: string;
143
+ created: number;
144
+ model: string;
145
+ system_fingerprint: string;
146
+ choices: OpenAIChoiceDelta[];
147
+ note?: string; // Rarely used, consider removing if not needed
148
+ }
149
+
150
+ interface OpenAIResponseNonStream {
151
+ id: string;
152
+ object: string;
153
+ created: number;
154
+ model: string;
155
+ choices: OpenAIChoiceNonStream[];
156
+ usage?: { // Placeholder for now
157
+ prompt_tokens: number;
158
+ completion_tokens: number;
159
+ total_tokens: number;
160
+ };
161
  }
162
 
163
  interface OpenAIModel {
164
+ id: string;
165
+ object: string;
166
+ created: number;
167
+ owned_by: string;
168
+ }
169
+
170
+ interface OpenAIModelsResponse {
171
+ object: string;
172
+ data: OpenAIModel[];
173
+ }
174
+
175
+ // Helper function to get Hunyuan model name from OpenAI model name
176
+ function getHunyuanModelName(openaiModelName: string): string {
177
+ switch (openaiModelName) {
178
+ case "hunyuan-turbos-latest":
179
+ return "hunyuan-turbos-latest";
180
+ case "hunyuan-t1-latest": // Fallthrough is intentional
181
+ default:
182
+ return "hunyuan-t1-latest";
183
+ }
184
+ }
185
+
186
+ async function hunyuanToOpenAIStream(
187
+ hunyuanResponse: Response,
188
+ openaiModelName: string,
189
+ ): Promise<ReadableStream<string>> {
190
+ const decoder = new TextDecoder("utf-8");
191
+ let buffer = "";
192
+
193
+ return new ReadableStream<string>({
194
+ async start(controller) {
195
+ if (!hunyuanResponse.body) {
196
+ controller.close();
197
+ return;
198
+ }
199
+ const reader = hunyuanResponse.body.getReader();
200
+
201
+ try {
202
+ while (true) {
203
+ const { done, value } = await reader.read();
204
+ if (done) { break; }
205
+ buffer += decoder.decode(value);
206
+
207
+ let boundary = buffer.indexOf("\n\n");
208
+ while (boundary !== -1) {
209
+ const chunk = buffer.substring(0, boundary).trim();
210
+ buffer = buffer.substring(boundary + 2);
211
+ boundary = buffer.indexOf("\n\n");
212
+
213
+ if (chunk.startsWith("data:")) {
214
+ const jsonStr = chunk.substring(5).trim();
215
+ if (jsonStr === "[DONE]") {
216
+ controller.enqueue(`data: [DONE]\n\n`);
217
+ continue;
218
+ }
219
+ try {
220
+ const hunyuanData = JSON.parse(jsonStr);
221
+ const openaiData: OpenAIStreamResponse = {
222
+ id: hunyuanData.id,
223
+ object: "chat.completion.chunk",
224
+ created: hunyuanData.created,
225
+ model: openaiModelName,
226
+ system_fingerprint: hunyuanData.system_fingerprint,
227
+ choices: hunyuanData.choices.map((choice): OpenAIChoiceDelta => ({
228
+ delta: {
229
+ role: choice.delta.role,
230
+ content: choice.delta.content,
231
+ reasoning_content: choice.delta.reasoning_content,
232
+ },
233
+ index: choice.index,
234
+ finish_reason: choice.finish_reason,
235
+ })),
236
+ };
237
+ controller.enqueue(`data: ${JSON.stringify(openaiData)}\n\n`);
238
+ } catch (error) {
239
+ console.error("Error parsing stream chunk:", error, jsonStr);
240
+ }
241
+ }
242
+ }
243
+ }
244
+ } finally {
245
+ reader.releaseLock();
246
+ controller.close();
247
+ }
248
+ },
249
+ });
250
+ }
251
+ async function hunyuanToOpenAINonStream(
252
+ hunyuanResponse: Response,
253
+ openaiModelName: string,
254
+ ): Promise<OpenAIResponseNonStream> {
255
+ const decoder = new TextDecoder("utf-8");
256
+ let buffer = "";
257
+ let allChoices: OpenAIChoiceNonStream[] = []; // Accumulate choices
258
+ let finalId = "";
259
+ let finalCreated = 0;
260
+ let finalModel = openaiModelName;
261
+
262
+
263
+ if (!hunyuanResponse.body) {
264
+ throw new Error("Hunyuan response body is empty.");
265
+ }
266
+ const reader = hunyuanResponse.body.getReader();
267
+
268
+ try {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
269
  while (true) {
270
+ const { done, value } = await reader.read();
271
+ if (done) {
272
+ break;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
273
  }
274
+ const text = decoder.decode(value);
275
+ buffer += text;
276
+
277
+ let boundary = buffer.indexOf("\n\n");
278
+ while (boundary !== -1) {
279
+ const chunk = buffer.substring(0, boundary).trim();
280
+ buffer = buffer.substring(boundary + 2);
281
+ boundary = buffer.indexOf("\n\n");
282
+
283
+ if (chunk.startsWith("data:")) {
284
+ const jsonStr = chunk.substring(5).trim();
285
+
286
+ if (jsonStr === "[DONE]") {
287
+ continue;
288
+ }
289
+
290
+ try {
291
+ const hunyuanData: OpenAIStreamResponse = JSON.parse(jsonStr); // Correct type
292
+ finalId = hunyuanData.id; // Get id and created from last chunk
293
+ finalCreated = hunyuanData.created;
294
+
295
+ // Accumulate choices, extracting content correctly.
296
+ hunyuanData.choices.forEach(choice => {
297
+ const existingChoice = allChoices.find(c => c.index === choice.index);
298
+ if (existingChoice) {
299
+ //append new content
300
+ existingChoice.message.content += choice.delta.content || "";
301
+ existingChoice.message.reasoning_content = (existingChoice.message.reasoning_content || "") + (choice.delta.reasoning_content || "");
302
+ if (choice.finish_reason) {
303
+ existingChoice.finish_reason = choice.finish_reason;
304
+ }
305
+ } else {
306
+ //new choice
307
+ allChoices.push({
308
+ message: {
309
+ role: choice.delta.role || "assistant", // Default to "assistant" if role is missing
310
+ content: choice.delta.content || "",
311
+ reasoning_content: choice.delta.reasoning_content,
312
+ },
313
+ index: choice.index,
314
+ finish_reason: choice.finish_reason,
315
+ });
316
+ }
317
+ });
318
+
319
+ } catch (error) {
320
+ console.error("Error parsing Hunyuan response chunk:", error, "Chunk:", jsonStr);
321
+ throw new Error(`Error parsing Hunyuan response: ${error}`);
322
+ }
323
+ }
324
+ }
325
+ }
326
+ } finally {
327
+ reader.releaseLock();
328
+ }
329
+
330
+ if (allChoices.length === 0) {
331
+ throw new Error("Failed to receive data from Hunyuan API.");
332
+ }
333
+
334
+ const openaiResponse: OpenAIResponseNonStream = {
335
+ id: finalId,
336
+ object: "chat.completion",
337
+ created: finalCreated,
338
+ model: finalModel,
339
+ choices: allChoices,
340
+ usage: { // Still placeholder, see notes below
341
+ prompt_tokens: 0,
342
+ completion_tokens: 0,
343
+ total_tokens: 0,
344
+ },
345
+ };
346
+ return openaiResponse;
347
+ }
348
+
349
+ async function handleChatCompletion(ctx: Context) {
350
+ try {
351
+ const authHeader = ctx.request.headers.get("Authorization");
352
+ if (!authHeader || !authHeader.startsWith("Bearer ")) {
353
+ ctx.response.status = 401;
354
+ ctx.response.body = { error: "Unauthorized: Missing or invalid API key" };
355
+ return;
356
+ }
357
+ const apiKey = authHeader.substring(7);
358
+
359
+ const body = await ctx.request.body({ type: "json" }).value;
360
+
361
+ if (!body || !body.messages || !Array.isArray(body.messages)) {
362
+ ctx.response.status = 400;
363
+ ctx.response.body = { error: "Invalid request body: 'messages' array is required." };
364
+ return;
365
+ }
366
+
367
+ const openaiModel = body.model || "hunyuan-t1-latest";
368
+ const hunyuanModel = getHunyuanModelName(openaiModel);
369
+ const stream = body.stream !== undefined ? body.stream : true;
370
+
371
+ const hunyuanMessages: HunyuanMessage[] = body.messages.map((msg: any) => ({
372
+ role: msg.role,
373
+ content: msg.content,
374
+ reasoning_content: msg.reasoning_content, // Pass through reasoning_content
375
+ }));
376
+
377
+ const hunyuanRequest: HunyuanRequest = {
378
+ stream: true, // Always stream to Hunyuan, then handle streaming/non-streaming for OpenAI
379
+ model: hunyuanModel,
380
+ query_id: crypto.randomUUID().replaceAll("-", ""),
381
+ messages: hunyuanMessages,
382
+ stream_moderation: true,
383
+ enable_enhancement: false,
384
+ };
385
+
386
+
387
+ const hunyuanResponse = await fetch(HUNYUAN_API_URL, {
388
+ method: "POST",
389
+ headers: {
390
+ "Host": "llm.hunyuan.tencent.com",
391
+ "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:136.0) Gecko/20100101 Firefox/136.0", // Consider making this configurable
392
+ "Accept": "*/*",
393
+ "Accept-Language": "en-US,en;q=0.5", // Consider making this configurable
394
+ "Accept-Encoding": "gzip, deflate, br, zstd",
395
+ "Referer": "https://llm.hunyuan.tencent.com/",
396
+ "Content-Type": "application/json",
397
+ "model": hunyuanModel, // Use the determined Hunyuan model
398
+ "polaris": "stream-server-online-sbs-10697",
399
+ "Authorization": `Bearer ${apiKey}`,
400
+ "Wsid": DEFAULT_WSID,
401
+ "staffname": DEFAULT_STAFFNAME,
402
+ "Origin": "https://llm.hunyuan.tencent.com",
403
+ "DNT": "1",
404
+ "Sec-GPC": "1",
405
+ "Connection": "keep-alive",
406
+ "Sec-Fetch-Dest": "empty",
407
+ "Sec-Fetch-Mode": "cors",
408
+ "Sec-Fetch-Site": "same-origin",
409
+ "Priority": "u=0",
410
+ "Pragma": "no-cache",
411
+ "Cache-Control": "no-cache",
412
+ "TE": "trailers",
413
+ },
414
+ body: JSON.stringify(hunyuanRequest),
415
+ });
416
+
417
+ if (!hunyuanResponse.ok) {
418
+ const errorText = await hunyuanResponse.text();
419
+ console.error("Hunyuan API error:", hunyuanResponse.status, errorText);
420
+ ctx.response.status = hunyuanResponse.status;
421
+ ctx.response.body = { error: `Hunyuan API error: ${hunyuanResponse.status} - ${errorText}` };
422
+ return;
423
  }
424
+
425
+ if (stream) {
426
+ const openaiStream = await hunyuanToOpenAIStream(hunyuanResponse, openaiModel);
427
+ ctx.response.body = openaiStream;
428
+ ctx.response.type = "text/event-stream";
429
+ } else {
430
+ const openaiResponse = await hunyuanToOpenAINonStream(hunyuanResponse, openaiModel);
431
+ ctx.response.body = openaiResponse;
432
+ ctx.response.type = "application/json";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
433
  }
434
+
435
+ } catch (error) {
436
+ console.error("Error in chat completion:", error);
437
+ ctx.response.status = 500;
438
+ ctx.response.body = { error: "Internal Server Error" };
 
 
439
  }
440
+ }
441
+
442
+ async function handleModels(ctx: Context) {
443
+ const models: OpenAIModelsResponse = {
444
+ object: "list",
445
+ data: [
446
+ {
447
+ id: "hunyuan-t1-latest",
448
+ object: "model",
449
+ created: Math.floor(Date.now() / 1000),
450
+ owned_by: "tencent",
451
+ },
452
+ {
453
+ id: "hunyuan-turbos-latest",
454
+ object: "model",
455
+ created: Math.floor(Date.now() / 1000), // Use current timestamp
456
+ owned_by: "tencent",
457
+ }
458
+ ],
459
+ };
460
+ ctx.response.body = models;
461
+ ctx.response.type = "application/json";
462
+ }
463
+
464
+ const sharedStyles = `
465
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
466
+
467
+ :root {
468
+ --background: #f0f2f5;
469
+ --foreground: #2e3440;
470
+ --primary: #5e81ac;
471
+ --primary-foreground: #eceff4;
472
+ --card: #ffffff;
473
+ --card-foreground: #2e3440;
474
+ --muted: #d8dee9;
475
+ --muted-foreground: #4c566a;
476
+ --border: #d8dee9;
477
+ --radius: 8px;
478
+ --header-bg: #3b4252;
479
+ --header-fg: #eceff4;
480
+ --link-color: #81a1c1;
481
+ }
482
+
483
+ * { margin: 0; padding: 0; box-sizing: border-box; }
484
+
485
+ body {
486
+ font-family: 'Inter', sans-serif;
487
+ background-color: var(--background);
488
+ color: var(--foreground);
489
+ display: flex;
490
+ flex-direction: column;
491
+ min-height: 100vh;
492
+ line-height: 1.6;
493
+ }
494
+
495
+ .header {
496
+ background-color: var(--header-bg);
497
+ color: var(--header-fg);
498
+ padding: 1rem 0;
499
+ width: 100%;
500
+ text-align: center;
501
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
502
+ }
503
+
504
+ .header-content {
505
+ display: flex;
506
+ justify-content: space-between;
507
+ align-items: center;
508
+ max-width: 48rem;
509
+ margin: 0 auto;
510
+ padding: 0 1rem;
511
+ }
512
+
513
+ .header a {
514
+ color: var(--header-fg);
515
+ text-decoration: none;
516
+ margin: 0 1rem;
517
+ font-weight: 500;
518
+ transition: color 0.2s;
519
+ }
520
+
521
+ .header a:hover {
522
+ color: var(--link-color);
523
+ }
524
+ .branding {
525
+ font-size: 1.25rem;
526
+ font-weight: 600;
527
+ }
528
+
529
+
530
+ .container {
531
+ width: 100%;
532
+ max-width: 48rem;
533
+ margin: 1.5rem auto;
534
+ padding: 0 1rem;
535
+ flex-grow: 1;
536
+ }
537
+
538
+ .card {
539
+ background-color: var(--card);
540
+ border-radius: var(--radius);
541
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
542
+ padding: 1.5rem;
543
+ margin-bottom: 1.5rem;
544
+ }
545
+
546
+ h1 {
547
+ font-size: 2.25rem;
548
+ font-weight: 700;
549
+ margin-bottom: 1rem;
550
+ color: var(--foreground);
551
+ text-align: center;
552
+ }
553
+
554
+ h2 {
555
+ font-size: 1.75rem;
556
+ font-weight: 600;
557
+ margin-bottom: 1rem;
558
+ color: var(--foreground);
559
+ }
560
+
561
+ h3 {
562
+ font-size: 1.25rem;
563
+ font-weight: 600;
564
+ margin-top: 1rem;
565
+ margin-bottom: 0.5rem;
566
+ color: var(--foreground);
567
+ }
568
+
569
+ p {
570
+ color: var(--muted-foreground);
571
+ font-size: 1rem;
572
+ margin-bottom: 1rem;
573
+ line-height: 1.5;
574
+ }
575
+
576
+ a {
577
+ color: var(--link-color);
578
+ text-decoration: none;
579
+ }
580
+ a:hover {
581
+ text-decoration: underline;
582
+ }
583
+
584
+ pre {
585
+ background-color: var(--muted);
586
+ padding: 1rem;
587
+ border-radius: var(--radius);
588
+ overflow-x: auto;
589
+ margin-bottom: 1rem;
590
+ border: 1px solid var(--border);
591
+ line-height: 1.4;
592
+ }
593
+
594
+ code {
595
+ font-family: 'Courier New', Courier, monospace;
596
+ font-size: 0.875rem;
597
+ }
598
+
599
+ .button {
600
+ display: inline-flex;
601
+ align-items: center;
602
+ justify-content: center;
603
+ white-space: nowrap;
604
+ border-radius: var(--radius);
605
+ height: 2.75rem;
606
+ padding: 0 1.25rem;
607
+ font-size: 1rem;
608
+ font-weight: 500;
609
+ transition: all 0.2s;
610
+ cursor: pointer;
611
+ text-decoration: none;
612
+ background-color: var(--primary);
613
+ color: var(--primary-foreground);
614
+ border: none;
615
+ }
616
+
617
+ .button:hover {
618
+ opacity: 0.9;
619
+ }
620
+
621
+ .footer {
622
+ margin-top: auto;
623
+ padding: 1rem 0;
624
+ text-align: center;
625
+ color: var(--muted-foreground);
626
+ border-top: 1px solid var(--border);
627
+ width: 100%;
628
+ }
629
+
630
+ .footer a {
631
+ color: var(--link-color);
632
+ }
633
+ `;
634
+
635
+
636
+ const header = `
637
+ <div class="header">
638
+ <div class="header-content">
639
+ <span class="branding">Hunyuan Proxy</span>
640
+ <div>
641
+ <a href="/">Home</a>
642
+ <a href="/playground">Playground</a>
643
+ <a href="/docs">Docs</a>
644
+ <a href="/getkey">Get API Key</a>
645
+ </div>
646
+ </div>
647
+ </div>
648
+ `;
649
+
650
+
651
+ const homePage = `
652
+ <!DOCTYPE html>
653
+ <html lang="en">
654
+ <head>
655
+ <meta charset="UTF-8">
656
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
657
+ <title>Hunyuan OpenAI Proxy</title>
658
+ <style>${sharedStyles}</style>
659
+ </head>
660
+ <body>
661
+ ${header}
662
+ <div class="container">
663
+ <h1>Hunyuan OpenAI Proxy</h1>
664
+ <div class="card">
665
+ <h2>Welcome</h2>
666
+ <p>This is a proxy server that converts the Tencent Hunyuan LLM API to an OpenAI-compatible API.</p>
667
+ <p>You can use this proxy to access the Hunyuan LLM with any OpenAI-compatible client.</p>
668
+ </div>
669
+ </div>
670
+ <div class="footer">
671
+ <p>Powered by <a href="https://neuroplexus.my" target="_blank">Neuroplexus</a></p>
672
+ </div>
673
+ </body>
674
+ </html>
675
+ `;
676
+
677
+
678
+ const playgroundPage = `
679
+ <!DOCTYPE html>
680
+ <html lang="en">
681
+ <head>
682
+ <meta charset="UTF-8">
683
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
684
+ <title>Hunyuan Playground</title>
685
+ <style>${sharedStyles}
686
+ textarea, #output {
687
+ width: 100%;
688
+ border: 1px solid var(--border);
689
+ border-radius: var(--radius);
690
+ padding: 1rem;
691
+ margin-bottom: 1rem;
692
+ font-family: inherit;
693
+ font-size: 1rem;
694
+ resize: vertical;
695
+ color: var(--foreground);
696
  }
697
+ textarea { min-height: 10rem; }
698
+ #output { min-height: 15rem; background-color: var(--muted); overflow-y: auto; }
699
+ .button { width: 100%; }
700
+
701
+ </style>
702
+ </head>
703
+ <body>
704
+ ${header}
705
+ <div class="container">
706
+ <h1>Hunyuan Playground</h1>
707
+ <div class="card">
708
+ <textarea id="input" placeholder="Enter your prompt here..."></textarea>
709
+ <button class="button" onclick="sendMessage()">Send</button>
710
+ <div id="output"></div>
711
+ </div>
712
+ </div>
713
+ <div class="footer">
714
+ <p>Powered by <a href="https://neuroplexus.my" target="_blank">Neuroplexus</a></p>
715
+ </div>
716
+ <script>
717
+ const apiKey = "${API_KEY}"; // Consider making this dynamic
718
+ async function sendMessage() {
719
+ const input = document.getElementById('input').value;
720
+ const outputDiv = document.getElementById('output');
721
+ outputDiv.innerHTML = '';
722
+
723
+ const response = await fetch('/v1/chat/completions', {
724
+ method: 'POST',
725
+ headers: {
726
+ 'Content-Type': 'application/json',
727
+ 'Authorization': 'Bearer ' + apiKey,
728
+ },
729
+ body: JSON.stringify({
730
+ messages: [{ role: 'user', content: input }],
731
+ stream: true,
732
+ }),
733
+ });
734
+
735
+ if (!response.ok) {
736
+ outputDiv.innerHTML = 'Error: ' + response.statusText;
737
+ return;
738
+ }
739
+
740
+ const reader = response.body.getReader();
741
+ const decoder = new TextDecoder('utf-8');
742
+ let buffer = '';
743
+
744
+ try {
745
+ while (true) {
746
+ const { done, value } = await reader.read();
747
+ if (done) break;
748
+
749
+ buffer += decoder.decode(value, { stream: true });
750
+
751
+ let boundary = buffer.indexOf("\\n\\n");
752
+ while (boundary !== -1) {
753
+ const chunk = buffer.substring(0, boundary).trim();
754
+ buffer = buffer.substring(boundary + 2);
755
+ boundary = buffer.indexOf("\\n\\n");
756
+
757
+ if (chunk.startsWith("data:")) {
758
+ const jsonStr = chunk.substring(5).trim();
759
+
760
+ if (jsonStr === "[DONE]") {
761
+ continue
762
+ }
763
+ try {
764
+ const data = JSON.parse(jsonStr);
765
+ if (data.choices && data.choices[0] && data.choices[0].delta && data.choices[0].delta.content) {
766
+ outputDiv.innerHTML += data.choices[0].delta.content;
767
+ outputDiv.scrollTop = outputDiv.scrollHeight;
768
+ }
769
+ } catch (error) {
770
+ console.error("Error parsing JSON:", error);
771
+ outputDiv.innerHTML += "Error parsing response chunk. ";
772
+ }
773
+ }
774
+ }
775
+ }
776
+ } finally {
777
+ reader.releaseLock();
778
+ }
779
  }
780
+ </script>
781
+ </body>
782
+ </html>
783
+ `;
784
+
785
+ const docsPage = `
786
+ <!DOCTYPE html>
787
+ <html lang="en">
788
+ <head>
789
+ <meta charset="UTF-8">
790
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
791
+ <title>API Documentation</title>
792
+ <style>${sharedStyles}</style>
793
+ </head>
794
+ <body>
795
+ ${header}
796
+ <div class="container">
797
+ <h1>API Documentation</h1>
798
+ <div class="card">
799
+ <h2>Chat Completions</h2>
800
+ <p>This endpoint mimics the OpenAI Chat Completion API.</p>
801
+ <h3>Endpoint</h3>
802
+ <pre><code>POST /v1/chat/completions</code></pre>
803
+ <h3>Request Headers</h3>
804
+ <pre><code>Authorization: Bearer YOUR_API_KEY</code></pre>
805
+ <pre><code>Content-Type: application/json</code></pre>
806
+
807
+ <h3>Request Body (Example)</h3>
808
+ <pre><code>{
809
+ "messages": [
810
+ {
811
+ "role": "user",
812
+ "content": "Hello, who are you?"
813
  }
814
+ ],
815
+ "model": "hunyuan-t1-latest",
816
+ "stream": true
817
+ }</code></pre>
818
+ <p>Supported models: <code>hunyuan-t1-latest</code>, <code>hunyuan-turbos-latest</code>. To make a non-streaming request, set <code>"stream": false</code> in the request body.</p>
819
+ <h3>Response</h3>
820
+ <p>Returns a stream of Server-Sent Events (SSE) in the OpenAI format for streaming requests, or a JSON object for non-streaming requests.</p>
821
+ </div>
822
+
823
+ <div class="card">
824
+ <h2>Models</h2>
825
+ <p>Get a list of available models.</p>
826
+ <h3>Endpoint</h3>
827
+ <pre><code>GET /v1/models</code></pre>
828
+ <h3>Response (Example)</h3>
829
+ <pre><code>
830
+ {
831
+ "object": "list",
832
+ "data": [
833
+ {
834
+ "id": "hunyuan-t1-latest",
835
+ "object": "model",
836
+ "created": 1678886400,
837
+ "owned_by": "tencent"
 
 
 
 
 
 
 
 
838
  },
839
+ {
840
+ "id": "hunyuan-turbos-latest",
841
+ "object": "model",
842
+ "created": 1700000000,
843
+ "owned_by": "tencent"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
844
  }
845
+ ]
846
+ }
847
+ </code></pre>
848
+ </div>
849
+ <div class="card">
850
+ <h2>Get API Key</h2>
851
+ <p>Retrieves the API key.</p>
852
+ <h3>Endpoint</h3>
853
+ <pre><code>GET /getkey</code></pre>
854
+ </div>
855
+ </div>
856
+ <div class="footer">
857
+ <p>Powered by <a href="https://neuroplexus.my" target="_blank">Neuroplexus</a></p>
858
+ </div>
859
+ </body>
860
+ </html>
861
+ `;
862
+
863
+ const getKeyPage = `
864
+ <!DOCTYPE html>
865
+ <html lang="en">
866
+ <head>
867
+ <meta charset="UTF-8">
868
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
869
+ <title>Get API Key</title>
870
+ <style>${sharedStyles}</style>
871
+ </head>
872
+ <body>
873
+ ${header}
874
+ <div class="container">
875
+ <h1>Get API Key</h1>
876
+ <div class="card">
877
+ <p>Your API Key is: <code>${API_KEY}</code></p>
878
+ </div>
879
+ </div>
880
+ <div class="footer">
881
+ <p>Powered by <a href="https://neuroplexus.my" target="_blank">Neuroplexus</a></p>
882
+ </div>
883
+ </body>
884
+ </html>
885
+ `;
886
+ async function handleGetKey(ctx: Context) {
887
+ const acceptHeader = ctx.request.headers.get("Accept");
888
+ if (acceptHeader && acceptHeader.includes("application/json")) {
889
+ ctx.response.body = { key: API_KEY };
890
+ ctx.response.type = "application/json";
891
+ } else {
892
+ ctx.response.body = getKeyPage;
893
+ ctx.response.type = "text/html";
894
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
895
  }
896
 
897
+ async function handleHomePage(ctx: Context) {
898
+ ctx.response.body = homePage;
899
+ ctx.response.type = "text/html";
900
+ }
901
+
902
+ async function handlePlayground(ctx: Context) {
903
+ ctx.response.body = playgroundPage;
904
+ ctx.response.type = "text/html";
905
+ }
906
+
907
+ async function handleDocs(ctx: Context) {
908
+ ctx.response.body = docsPage;
909
+ ctx.response.type = "text/html";
910
+ }
911
+
912
+ const router = new Router();
913
+ router.post("/v1/chat/completions", handleChatCompletion);
914
+ router.get("/v1/models", handleModels);
915
+ router.get("/getkey", handleGetKey);
916
+ router.get("/", handleHomePage);
917
+ router.get("/playground", handlePlayground);
918
+ router.get("/docs", handleDocs);
919
+
920
+ const app = new Application();
921
+ app.use(router.routes());
922
+ app.use(router.allowedMethods());
923
+
924
+ console.log("Server listening on port 8000");
925
+ await app.listen({ port: 8000 });