http.go 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296
  1. package main
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "html"
  6. "io"
  7. "net/http"
  8. "strings"
  9. "time"
  10. )
  11. const minimalHTML = `<!DOCTYPE html>
  12. <html>
  13. <head>
  14. <title>ch.at</title>
  15. <style>
  16. body { text-align: center; margin: 40px; }
  17. pre { text-align: left; max-width: 600px; margin: 20px auto; padding: 20px;
  18. white-space: pre-wrap; word-wrap: break-word; }
  19. input[type="text"] { width: 300px; }
  20. </style>
  21. </head>
  22. <body>
  23. <h1>ch.at</h1>
  24. <p><i>pronounced "ch-dot-at"</i></p>
  25. <pre>%s</pre>
  26. <form method="POST" action="/">
  27. <input type="text" name="q" placeholder="Type your message..." autofocus>
  28. <textarea name="h" style="display:none">%s</textarea>
  29. <input type="submit" value="Send">
  30. </form>
  31. <p><a href="/">Clear History</a> • <a href="https://github.com/Deep-ai-inc/ch.at#readme">About</a></p>
  32. </body>
  33. </html>`
  34. func StartHTTPServer(port int) error {
  35. http.HandleFunc("/", handleRoot)
  36. http.HandleFunc("/v1/chat/completions", handleChatCompletions)
  37. addr := fmt.Sprintf(":%d", port)
  38. fmt.Printf("HTTP server listening on %s\n", addr)
  39. return http.ListenAndServe(addr, nil)
  40. }
  41. func StartHTTPSServer(port int, certFile, keyFile string) error {
  42. addr := fmt.Sprintf(":%d", port)
  43. fmt.Printf("HTTPS server listening on %s\n", addr)
  44. return http.ListenAndServeTLS(addr, certFile, keyFile, nil)
  45. }
  46. func handleRoot(w http.ResponseWriter, r *http.Request) {
  47. if !rateLimitAllow(r.RemoteAddr) {
  48. http.Error(w, "Rate limit exceeded", http.StatusTooManyRequests)
  49. return
  50. }
  51. var query, history, prompt string
  52. content := ""
  53. jsonResponse := ""
  54. if r.Method == "POST" {
  55. if err := r.ParseForm(); err != nil {
  56. http.Error(w, "Failed to parse form", http.StatusBadRequest)
  57. return
  58. }
  59. query = r.FormValue("q")
  60. history = r.FormValue("h")
  61. // Limit history size to prevent abuse
  62. if len(history) > 2048 {
  63. history = history[len(history)-2048:]
  64. }
  65. // If no form fields, treat body as raw query (for curl)
  66. if query == "" {
  67. body, err := io.ReadAll(io.LimitReader(r.Body, 4096)) // Limit body size
  68. if err != nil {
  69. http.Error(w, "Failed to read request body", http.StatusBadRequest)
  70. return
  71. }
  72. query = string(body)
  73. }
  74. } else {
  75. query = r.URL.Query().Get("q")
  76. // Also support path-based queries like /what-is-go
  77. if query == "" && r.URL.Path != "/" {
  78. query = strings.ReplaceAll(strings.TrimPrefix(r.URL.Path, "/"), "-", " ")
  79. }
  80. }
  81. if query != "" {
  82. // Build prompt with history
  83. prompt = query
  84. if history != "" {
  85. prompt = history + "Q: " + query
  86. }
  87. response, err := LLM(prompt, nil)
  88. if err != nil {
  89. content = err.Error()
  90. errJSON, _ := json.Marshal(map[string]string{"error": err.Error()})
  91. jsonResponse = string(errJSON)
  92. } else {
  93. // Store JSON response
  94. respJSON, _ := json.Marshal(map[string]string{
  95. "question": query,
  96. "answer": response,
  97. })
  98. jsonResponse = string(respJSON)
  99. // Append to history
  100. newExchange := fmt.Sprintf("Q: %s\nA: %s\n\n", query, response)
  101. if history != "" {
  102. content = history + newExchange
  103. } else {
  104. content = newExchange
  105. }
  106. // Trim history if too long (UTF-8 safe)
  107. if len(content) > 2048 {
  108. // Keep roughly last 600 characters (UTF-8 safe)
  109. runes := []rune(content)
  110. if len(runes) > 600 {
  111. content = string(runes[len(runes)-600:])
  112. }
  113. }
  114. }
  115. } else if history != "" {
  116. content = history
  117. }
  118. accept := r.Header.Get("Accept")
  119. wantsJSON := strings.Contains(accept, "application/json")
  120. wantsHTML := strings.Contains(accept, "text/html")
  121. wantsStream := strings.Contains(accept, "text/event-stream")
  122. // Stream for curl when requested
  123. if wantsStream && query != "" {
  124. w.Header().Set("Content-Type", "text/event-stream")
  125. w.Header().Set("Cache-Control", "no-cache")
  126. w.Header().Set("Connection", "keep-alive")
  127. flusher, ok := w.(http.Flusher)
  128. if !ok {
  129. http.Error(w, "Streaming not supported", http.StatusInternalServerError)
  130. return
  131. }
  132. // Stream response
  133. ch := make(chan string)
  134. go func() {
  135. if _, err := LLM(prompt, ch); err != nil {
  136. // Send error as SSE event
  137. fmt.Fprintf(w, "data: Error: %s\n\n", err.Error())
  138. flusher.Flush()
  139. }
  140. }()
  141. for chunk := range ch {
  142. fmt.Fprintf(w, "data: %s\n\n", chunk)
  143. flusher.Flush()
  144. }
  145. fmt.Fprintf(w, "data: [DONE]\n\n")
  146. return
  147. }
  148. // Return JSON for API requests, HTML for browsers, plain text for curl
  149. if wantsJSON && jsonResponse != "" {
  150. w.Header().Set("Content-Type", "application/json; charset=utf-8")
  151. fmt.Fprint(w, jsonResponse)
  152. } else if wantsHTML {
  153. w.Header().Set("Content-Type", "text/html; charset=utf-8")
  154. fmt.Fprintf(w, minimalHTML, html.EscapeString(content), html.EscapeString(content))
  155. } else {
  156. // Default to plain text for curl and other tools
  157. w.Header().Set("Content-Type", "text/plain; charset=utf-8")
  158. fmt.Fprint(w, content)
  159. }
  160. }
  161. type ChatRequest struct {
  162. Model string `json:"model"`
  163. Messages []Message `json:"messages"`
  164. Stream bool `json:"stream,omitempty"`
  165. }
  166. type Message struct {
  167. Role string `json:"role"`
  168. Content string `json:"content"`
  169. }
  170. type ChatResponse struct {
  171. ID string `json:"id"`
  172. Object string `json:"object"`
  173. Created int64 `json:"created"`
  174. Model string `json:"model"`
  175. Choices []Choice `json:"choices"`
  176. }
  177. type Choice struct {
  178. Index int `json:"index"`
  179. Message Message `json:"message"`
  180. }
  181. func handleChatCompletions(w http.ResponseWriter, r *http.Request) {
  182. if !rateLimitAllow(r.RemoteAddr) {
  183. http.Error(w, "Rate limit exceeded", http.StatusTooManyRequests)
  184. return
  185. }
  186. if r.Method != "POST" {
  187. http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
  188. return
  189. }
  190. var req ChatRequest
  191. if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
  192. http.Error(w, "Invalid JSON", http.StatusBadRequest)
  193. return
  194. }
  195. // Convert messages to format for LLM
  196. messages := make([]map[string]string, len(req.Messages))
  197. for i, msg := range req.Messages {
  198. messages[i] = map[string]string{
  199. "role": msg.Role,
  200. "content": msg.Content,
  201. }
  202. }
  203. if req.Stream {
  204. w.Header().Set("Content-Type", "text/event-stream")
  205. w.Header().Set("Cache-Control", "no-cache")
  206. w.Header().Set("Connection", "keep-alive")
  207. flusher, ok := w.(http.Flusher)
  208. if !ok {
  209. http.Error(w, "Streaming not supported", http.StatusInternalServerError)
  210. return
  211. }
  212. // Stream response
  213. ch := make(chan string)
  214. go LLM(messages, ch)
  215. for chunk := range ch {
  216. resp := map[string]interface{}{
  217. "id": fmt.Sprintf("chatcmpl-%d", time.Now().Unix()),
  218. "object": "chat.completion.chunk",
  219. "created": time.Now().Unix(),
  220. "model": req.Model,
  221. "choices": []map[string]interface{}{{
  222. "index": 0,
  223. "delta": map[string]string{"content": chunk},
  224. }},
  225. }
  226. data, err := json.Marshal(resp)
  227. if err != nil {
  228. fmt.Fprintf(w, "data: Failed to marshal response\n\n")
  229. return
  230. }
  231. fmt.Fprintf(w, "data: %s\n\n", data)
  232. flusher.Flush()
  233. }
  234. fmt.Fprintf(w, "data: [DONE]\n\n")
  235. } else {
  236. response, err := LLM(messages, nil)
  237. if err != nil {
  238. http.Error(w, err.Error(), http.StatusInternalServerError)
  239. return
  240. }
  241. chatResp := ChatResponse{
  242. ID: fmt.Sprintf("chatcmpl-%d", time.Now().Unix()),
  243. Object: "chat.completion",
  244. Created: time.Now().Unix(),
  245. Model: req.Model,
  246. Choices: []Choice{{
  247. Index: 0,
  248. Message: Message{
  249. Role: "assistant",
  250. Content: response,
  251. },
  252. }},
  253. }
  254. w.Header().Set("Content-Type", "application/json")
  255. json.NewEncoder(w).Encode(chatResp)
  256. }
  257. }