Skip to content

Commit 43dfeb7

Browse files
committed
Improve chat routing, qoder mapping, and server logs
Refactor chat route handlers: GET now returns a 400 instructing clients to use POST; POST adds stricter validation for messages, enhanced logging, and better mapping from OpenAI params to qodercli flags (maps max_tokens to qodercli --max-output-tokens, notes temperature unsupported). Stream and tool-call handling was cleaned up (SSE payload formatting, tool-call logging) and some unused/disruptive child-kill behavior removed. Server updates: increase JSON body limit to 10MB, add URL-encoded body support, add /v1 debug request logging middleware, and introduce a global error handler that logs and returns JSON error responses.
1 parent c8970b1 commit 43dfeb7

File tree

2 files changed

+72
-136
lines changed

2 files changed

+72
-136
lines changed

src/routes/chat.js

Lines changed: 47 additions & 135 deletions
Original file line numberDiff line numberDiff line change
@@ -23,148 +23,73 @@ const setSSEHeaders = (res) => {
2323
res.setHeader('X-Accel-Buffering', 'no');
2424
};
2525

26-
// Smart routing - support GET requests for better client compatibility
26+
// ── GET handler for client compatibility ────────────────────────────────────
2727
router.get('/', (req, res) => {
28-
// Debug: Log what we're actually receiving
29-
console.log('[GET /chat/completions] Query params:', req.query);
30-
console.log('[GET /chat/completions] Body:', req.body);
31-
console.log('[GET /chat/completions] Headers:', req.headers);
28+
console.log('[GET /chat/completions] Query:', req.query);
29+
console.log('[GET /chat/completions] User-Agent:', req.headers['user-agent']);
3230

33-
// Extract parameters from query string, body, or headers
34-
const { message, messages, model = 'auto', stream = false, temperature, max_tokens, content, text, prompt: userPrompt } = {
35-
...req.query,
36-
...req.body
37-
};
38-
39-
let parsedMessages;
40-
41-
// Try multiple parameter names that different bots might use
42-
const userInput = message || content || text || userPrompt || req.query.q || req.body.content;
43-
const messageArray = messages || req.body.messages;
44-
45-
if (messageArray) {
46-
// Try to parse messages from parameter (JSON string or already parsed)
47-
try {
48-
if (typeof messageArray === 'string') {
49-
parsedMessages = JSON.parse(decodeURIComponent(messageArray));
50-
} else {
51-
parsedMessages = messageArray;
31+
// Return helpful error - OpenAI SDK should use POST
32+
return res.status(400).json({
33+
error: {
34+
message: 'Use POST method for chat completions',
35+
type: 'invalid_request_error',
36+
help: 'POST /v1/chat/completions with JSON body: {"messages": [...], "model": "auto"}',
37+
debug: {
38+
receivedMethod: 'GET',
39+
expectedMethod: 'POST',
40+
userAgent: req.headers['user-agent']
5241
}
53-
} catch (e) {
54-
return res.status(400).json({
55-
error: {
56-
message: 'Invalid messages parameter. Must be valid JSON array.',
57-
type: 'invalid_request_error',
58-
}
59-
});
6042
}
61-
} else if (userInput) {
62-
// Simple single message support with various parameter names
63-
parsedMessages = [{ role: 'user', content: decodeURIComponent(userInput.toString()) }];
64-
} else {
65-
// If no recognized parameters, provide helpful debug info
43+
});
44+
});
45+
46+
// ── POST handler (standard OpenAI-compatible endpoint) ──────────────────────
47+
router.post('/', (req, res) => {
48+
// Debug logging
49+
console.log('[POST /chat/completions] Content-Type:', req.headers['content-type']);
50+
console.log('[POST /chat/completions] Body keys:', Object.keys(req.body || {}));
51+
52+
const { messages, model: requestedModel, stream = false, temperature, max_tokens, tools, tool_choice } = req.body || {};
53+
54+
// Validate messages
55+
if (!messages || !Array.isArray(messages) || messages.length === 0) {
6656
return res.status(400).json({
6757
error: {
68-
message: 'Missing required parameter. Use ?message=your_text or ?messages=[{"role":"user","content":"text"}]',
58+
message: 'messages is required and must be a non-empty array',
6959
type: 'invalid_request_error',
70-
help: 'GET Example: /v1/chat/completions?message=Hello&model=auto',
7160
debug: {
72-
receivedQuery: req.query,
7361
receivedBody: req.body,
74-
supportedParams: ['message', 'content', 'text', 'prompt', 'messages', 'q']
62+
bodyType: typeof req.body,
63+
contentType: req.headers['content-type']
7564
}
76-
}
77-
});
78-
}
79-
80-
if (!Array.isArray(parsedMessages) || parsedMessages.length === 0) {
81-
return res.status(400).json({
82-
error: {
83-
message: 'messages must be a non-empty array',
84-
type: 'invalid_request_error',
8565
},
8666
});
8767
}
8868

89-
const mappedModel = getModelMapping(model);
90-
const prompt = messagesToPrompt(parsedMessages);
91-
const id = newId('chatcmpl');
92-
93-
const flags = [];
94-
if (max_tokens != null) flags.push('--max-tokens', String(max_tokens));
95-
if (temperature != null) flags.push('--temperature', String(temperature));
96-
97-
const isStream = stream === 'true';
98-
99-
if (isStream) {
100-
setSSEHeaders(res);
101-
let lastFinishReason = 'stop';
102-
103-
const child = runQoderRequest({
104-
prompt,
105-
model: mappedModel,
106-
flags,
107-
timeoutMs: QODER_TIMEOUT_MS,
108-
onChunk: (data) => {
109-
const content = extractTextContent(data.message);
110-
const finishReason = data.message?.stop_reason || null;
111-
112-
if (finishReason) lastFinishReason = finishReason;
113-
114-
res.write(`data: ${JSON.stringify(buildStreamChunk(data, mappedModel, id))}\n\n`);
115-
},
116-
onError: (error) => {
117-
res.write(`data: ${JSON.stringify({ error: error.message })}\n\n`);
118-
res.write('data: [DONE]\n\n');
119-
res.end();
120-
},
121-
onEnd: () => {
122-
res.write(`data: ${JSON.stringify(buildDoneChunk())}\n\n`);
123-
res.write('data: [DONE]\n\n');
124-
res.end();
125-
},
126-
});
127-
128-
req.on('close', () => child.kill());
129-
} else {
130-
let fullContent = '';
131-
132-
const child = runQoderRequest({
133-
prompt,
134-
model: mappedModel,
135-
flags,
136-
timeoutMs: QODER_TIMEOUT_MS,
137-
onChunk: (data) => {
138-
const content = extractTextContent(data.message);
139-
if (content) fullContent += content;
140-
},
141-
onError: (error) => {
142-
res.status(500).json({ error: { message: error.message, type: 'server_error' } });
143-
},
144-
onEnd: () => {
145-
res.json(buildFullChatResponse(id, mappedModel, fullContent));
146-
},
147-
});
148-
}
149-
});
150-
151-
router.post('/', (req, res) => {
152-
15369
const model = getModelMapping(requestedModel);
15470
const prompt = messagesToPrompt(messages);
15571
const id = newId('chatcmpl');
72+
73+
console.log('[POST /chat/completions] Model:', model, 'Prompt length:', prompt.length, 'Stream:', stream);
15674

15775
const flags = [];
158-
if (max_tokens != null) flags.push('--max-tokens', String(max_tokens));
159-
if (temperature != null) flags.push('--temperature', String(temperature));
160-
161-
// Handle tool calling
76+
// Note: qodercli uses --max-output-tokens, not --max-tokens
77+
// And it only accepts specific values like "16k" or "32k"
78+
if (max_tokens != null) {
79+
// Convert OpenAI max_tokens to qodercli format
80+
if (max_tokens >= 32000) {
81+
flags.push('--max-output-tokens', '32k');
82+
} else if (max_tokens >= 16000) {
83+
flags.push('--max-output-tokens', '16k');
84+
}
85+
// Smaller values: qodercli will use its default
86+
}
87+
// Note: temperature is not supported by qodercli
88+
89+
// Log tool requests (not yet implemented)
16290
if (tools && Array.isArray(tools) && tools.length > 0) {
163-
// For now, we'll add tools support but qodercli uses its built-in tools
164-
// We could potentially map OpenAI tool specs to qodercli tools in the future
16591
console.log('[chat/completions] Tools requested but mapping not implemented yet');
16692
}
167-
16893
if (tool_choice && tool_choice !== 'auto') {
16994
console.log('[chat/completions] Tool choice specified but not implemented yet');
17095
}
@@ -185,30 +110,21 @@ router.post('/', (req, res) => {
185110

186111
if (finishReason) lastFinishReason = finishReason;
187112

188-
// Handle tool calls
189113
if (toolCalls && toolCalls.length > 0) {
190114
res.write(`data: ${JSON.stringify(buildToolCallStreamChunk(data, model, id))}\n\n`);
191115
lastFinishReason = 'tool_calls';
192-
}
193-
// Handle regular text content
194-
else if (content) {
116+
} else if (content) {
195117
res.write(`data: ${JSON.stringify(buildStreamChunk(content, model, id))}\n\n`);
196118
}
197119
},
198120
onDone: (_code, _stderr) => {
199-
// Send a final chunk with finish_reason so clients know why we stopped
200121
res.write(`data: ${JSON.stringify(buildDoneChunk(model, id, lastFinishReason))}\n\n`);
201122
res.write('data: [DONE]\n\n');
202123
res.end();
203124
},
204125
onError: (err) => {
205126
console.error('[chat/completions]', err.message);
206-
// Headers already sent — signal via SSE error event
207-
res.write(
208-
`data: ${JSON.stringify({
209-
error: { message: err.message, type: err.code === 'TIMEOUT' ? 'timeout_error' : 'api_error' },
210-
})}\n\n`
211-
);
127+
res.write(`data: ${JSON.stringify({ error: { message: err.message, type: err.code === 'TIMEOUT' ? 'timeout_error' : 'api_error' } })}\n\n`);
212128
res.end();
213129
},
214130
});
@@ -242,7 +158,6 @@ router.post('/', (req, res) => {
242158
});
243159
}
244160

245-
// Send response with tool calls if present
246161
if (allToolCalls.length > 0) {
247162
res.json(buildFullChatResponseWithTools(allToolCalls, fullContent, model, finishReason, id));
248163
} else {
@@ -255,9 +170,6 @@ router.post('/', (req, res) => {
255170
});
256171
},
257172
});
258-
259-
// For non-streaming, don't kill on client disconnect - let it complete
260-
// req.on('close', () => child.kill());
261173
}
262174
});
263175

src/server.js

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,21 @@ const app = express();
1616

1717
// ── Global middleware ────────────────────────────────────────────────────────
1818
app.use(cors({ origin: CORS_ORIGIN }));
19-
app.use(express.json());
19+
app.use(express.json({ limit: '10mb' })); // Increase body size limit
20+
app.use(express.urlencoded({ extended: true })); // Support URL-encoded bodies
2021
app.use(logger);
2122

23+
// Debug middleware - log ALL requests to v1 endpoints
24+
app.use('/v1', (req, res, next) => {
25+
console.log(`[V1 Request] ${req.method} ${req.originalUrl}`);
26+
console.log(`[V1 Request] Content-Type: ${req.headers['content-type']}`);
27+
console.log(`[V1 Request] Body present: ${!!req.body && Object.keys(req.body).length > 0}`);
28+
if (req.body && Object.keys(req.body).length > 0) {
29+
console.log(`[V1 Request] Body keys: ${Object.keys(req.body).join(', ')}`);
30+
}
31+
next();
32+
});
33+
2234
// ── Public routes ────────────────────────────────────────────────────────────
2335
app.get('/', (_req, res) => res.json({
2436
name: 'Qoder OpenAI Proxy',
@@ -40,6 +52,18 @@ app.use('/v1/chat/completions', chatRouter);
4052
app.use('/v1/completions', completionsRouter);
4153
app.use('/v1', v1Router);
4254

55+
// ── Global error handler ─────────────────────────────────────────────────────
56+
app.use((err, req, res, next) => {
57+
console.error('[Global Error Handler]', err.stack || err);
58+
res.status(500).json({
59+
error: {
60+
message: err.message || 'Internal server error',
61+
type: 'server_error',
62+
stack: process.env.NODE_ENV !== 'production' ? err.stack : undefined
63+
}
64+
});
65+
});
66+
4367
// ── Startup ──────────────────────────────────────────────────────────────────
4468
const start = async () => {
4569
const version = await checkQoderCli();

0 commit comments

Comments
 (0)