-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathserver.js
More file actions
145 lines (120 loc) · 4.89 KB
/
Copy pathserver.js
File metadata and controls
145 lines (120 loc) · 4.89 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
// Server-side API proxy for YC Office Hours
// Works both locally (node server.js) and on Vercel as a Serverless Function
//
// The frontend sends API requests to /api/chat on the same domain,
// eliminating browser CORS issues entirely.
const express = require('express');
const { OpenAI } = require('openai');
const path = require('path');
const app = express();
app.use(express.json());
// ========== Chat Completion (streaming) ==========
app.post('/api/chat', async (req, res) => {
const { messages, systemPrompt, apiKey, baseUrl, model, max_tokens, temperature, extraHeaders } = req.body;
if (!apiKey) {
return res.status(400).json({ error: 'API Key is required' });
}
if (!model) {
return res.status(400).json({ error: 'Model ID is required' });
}
// Sanitize base URL
let sanitizedUrl = (baseUrl || 'https://api.openai.com/v1')
.replace(/\/chat\/completions$/i, '')
.replace(/\/+$/, '');
const client = new OpenAI({
apiKey,
baseURL: sanitizedUrl,
defaultHeaders: extraHeaders || {},
maxRetries: 0,
});
// SSE headers
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.setHeader('X-Accel-Buffering', 'no');
// Build messages array
const msgs = [];
if (systemPrompt) msgs.push({ role: 'system', content: systemPrompt });
msgs.push(...messages);
try {
const stream = await client.chat.completions.create({
model,
messages: msgs,
stream: true,
max_tokens: max_tokens || 4000,
temperature: temperature ?? 0.7,
});
for await (const chunk of stream) {
const content = chunk.choices[0]?.delta?.content || '';
if (content) {
// Send standard OpenAI SSE format so the frontend's readOpenAIStream can parse it
res.write(`data: ${JSON.stringify({ choices: [{ delta: { content } }] })}\n\n`);
}
}
res.write('data: [DONE]\n\n');
res.end();
} catch (err) {
console.error('API Error:', err.message);
if (!res.headersSent) {
// Map common errors to user-friendly messages
let status = err.status || 500;
let message = err.message;
if (status === 401 || status === 403) message = 'API Key invalid or expired. Please check your key.';
else if (status === 404) message = `Model "${model}" not found or Base URL incorrect.`;
else if (status === 429) message = 'Rate limited — please wait a moment and try again.';
res.status(status).json({ error: message });
} else {
res.write(`data: ${JSON.stringify({ error: err.message })}\n\n`);
res.end();
}
}
});
// ========== Test Connection ==========
app.post('/api/test-connection', async (req, res) => {
const { apiKey, baseUrl, model } = req.body;
if (!apiKey) {
return res.status(400).json({ success: false, message: 'API Key is required' });
}
let sanitizedUrl = (baseUrl || 'https://api.openai.com/v1')
.replace(/\/chat\/completions$/i, '')
.replace(/\/+$/, '');
const testModel = model || 'gpt-4o';
const client = new OpenAI({ apiKey, baseURL: sanitizedUrl, timeout: 15000 });
try {
await client.chat.completions.create({
model: testModel,
messages: [{ role: 'user', content: 'Hi' }],
max_tokens: 5,
});
res.json({ success: true, message: 'Connected. The model accepted a test request.' });
} catch (err) {
console.error('Test connection error:', err.message);
let message = err.message;
const status = err.status || err.statusCode;
if (status === 401 || status === 403) message = 'API Key invalid or expired.';
else if (status === 404) message = `Model "${testModel}" not found or Base URL incorrect.`;
else if (status === 429) message = 'Rate limited — please wait a moment and try again.';
else if (err.code === 'ECONNREFUSED' || err.code === 'ENOTFOUND') message = `Cannot reach ${sanitizedUrl}`;
else if (err.code === 'ETIMEDOUT' || message.includes('timeout')) message = 'Connection timed out.';
res.status(status || 500).json({ success: false, message });
}
});
// ========== Static Files (for local dev) ==========
app.use(express.static(path.join(__dirname, 'public')));
// Fallback: serve public/index.html for SPA routing (non-API paths)
app.get('*', (req, res) => {
if (req.path.startsWith('/api/')) {
return res.status(404).json({ error: 'API endpoint not found' });
}
res.sendFile(path.join(__dirname, 'public', 'index.html'));
});
// ========== Start (local dev only; Vercel uses module.exports) ==========
const PORT = process.env.PORT || 3456;
if (!process.env.VERCEL && process.env.NODE_ENV !== 'production') {
app.listen(PORT, () => {
console.log(`\n YC Office Hours server running at http://localhost:${PORT}\n`);
console.log(` API proxy at http://localhost:${PORT}/api/chat`);
console.log(` Open http://localhost:${PORT} in your browser.\n`);
});
}
module.exports = app;