@@ -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 ────────────────────────────────────
2727router . 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
0 commit comments