http.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405
  1. package main
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "html"
  6. "io"
  7. "net/http"
  8. "strings"
  9. "time"
  10. )
  11. const htmlPromptPrefix = "You are a helpful assistant. Use HTML formatting instead of markdown (no CSS or style attributes): "
  12. const htmlHeader = `<!DOCTYPE html>
  13. <html>
  14. <head>
  15. <title>ch.at</title>
  16. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  17. <style>
  18. body { text-align: center; margin: 2.5rem; }
  19. .chat { text-align: left; max-width: 600px; margin: 1.25rem auto; }
  20. .q { padding: 1.25rem; background: #EEE; font-style: italic; font-size: large; }
  21. .a { padding: 0.5rem 1.25rem; }
  22. form { max-width: 568px; margin: 0 auto 3rem; display: flex; gap: .5rem; }
  23. input[type="text"] { width: 100%; padding: .5rem; }
  24. input[type="submit"] { padding: .5rem; }
  25. </style>
  26. </head>
  27. <body>
  28. <h1>ch.at</h1>
  29. <p>Universal Basic Intelligence</p>
  30. <p><small><i>pronounced "ch-dot-at"</i></small></p>
  31. <div class="chat">`
  32. const htmlFooterTemplate = `</div>
  33. <form method="POST" action="/">
  34. <input type="text" name="q" placeholder="Type your message..." autofocus>
  35. <input type="submit" value="Send">
  36. <textarea name="h" style="display:none">%s</textarea>
  37. </form>
  38. <p><a href="/">New Chat</a></p>
  39. <p><small>
  40. Also available: ssh ch.at • curl ch.at/?q=hello • dig @ch.at "question" TXT<br>
  41. No logs • No accounts • Free software • <a href="https://github.com/Deep-ai-inc/ch.at">GitHub</a>
  42. </small></p>
  43. </body>
  44. </html>`
  45. func StartHTTPServer(port int) error {
  46. http.HandleFunc("/", handleRoot)
  47. http.HandleFunc("/v1/chat/completions", handleChatCompletions)
  48. addr := fmt.Sprintf(":%d", port)
  49. return http.ListenAndServe(addr, nil)
  50. }
  51. func StartHTTPSServer(port int, certFile, keyFile string) error {
  52. addr := fmt.Sprintf(":%d", port)
  53. return http.ListenAndServeTLS(addr, certFile, keyFile, nil)
  54. }
  55. func handleRoot(w http.ResponseWriter, r *http.Request) {
  56. w.Header().Set("Access-Control-Allow-Origin", "*")
  57. if !rateLimitAllow(r.RemoteAddr) {
  58. http.Error(w, "Rate limit exceeded", http.StatusTooManyRequests)
  59. return
  60. }
  61. var query, history, prompt string
  62. content := ""
  63. jsonResponse := ""
  64. if r.Method == "POST" {
  65. if err := r.ParseForm(); err != nil {
  66. http.Error(w, "Failed to parse form", http.StatusBadRequest)
  67. return
  68. }
  69. query = r.FormValue("q")
  70. history = r.FormValue("h")
  71. // Limit history size to ensure compatibility
  72. if len(history) > 65536 {
  73. history = history[len(history)-65536:]
  74. }
  75. if query == "" {
  76. body, err := io.ReadAll(io.LimitReader(r.Body, 65536)) // Limit body size
  77. if err != nil {
  78. http.Error(w, "Failed to read request body", http.StatusBadRequest)
  79. return
  80. }
  81. query = string(body)
  82. }
  83. } else {
  84. query = r.URL.Query().Get("q")
  85. // Support path-based queries like /what-is-go
  86. if query == "" && r.URL.Path != "/" {
  87. query = strings.ReplaceAll(strings.TrimPrefix(r.URL.Path, "/"), "-", " ")
  88. }
  89. }
  90. accept := r.Header.Get("Accept")
  91. wantsJSON := strings.Contains(accept, "application/json")
  92. wantsHTML := strings.Contains(accept, "text/html")
  93. wantsStream := strings.Contains(accept, "text/event-stream")
  94. if query != "" {
  95. prompt = query
  96. if history != "" {
  97. prompt = history + "Q: " + query
  98. }
  99. if wantsHTML && r.Header.Get("Accept") != "application/json" {
  100. w.Header().Set("Content-Type", "text/html; charset=utf-8")
  101. w.Header().Set("Transfer-Encoding", "chunked")
  102. w.Header().Set("X-Accel-Buffering", "no")
  103. w.Header().Set("Cache-Control", "no-cache")
  104. flusher := w.(http.Flusher)
  105. headerSize := len(htmlHeader)
  106. historySize := len(html.EscapeString(history))
  107. querySize := len(html.EscapeString(query))
  108. currentSize := headerSize + historySize + querySize + 10
  109. const minThreshold = 6144
  110. fmt.Fprint(w, htmlHeader)
  111. if currentSize < minThreshold {
  112. paddingNeeded := (minThreshold - currentSize) / 3
  113. if paddingNeeded > 0 {
  114. padding := strings.Repeat("\u200B", paddingNeeded)
  115. fmt.Fprint(w, padding)
  116. }
  117. }
  118. if history != "" {
  119. histParts := strings.Split("\n"+history, "\nQ: ")
  120. for _, part := range histParts[1:] {
  121. if i := strings.Index(part, "\nA: "); i >= 0 {
  122. question := part[:i]
  123. answer := part[i+4:]
  124. answer = strings.TrimRight(answer, "\n")
  125. fmt.Fprintf(w, "<div class=\"q\">%s</div>\n", html.EscapeString(question))
  126. fmt.Fprintf(w, "<div class=\"a\">%s</div>\n", answer)
  127. }
  128. }
  129. }
  130. fmt.Fprintf(w, "<div class=\"q\">%s</div>\n<div class=\"a\">", html.EscapeString(query))
  131. flusher.Flush()
  132. ch := make(chan string)
  133. go func() {
  134. htmlPrompt := htmlPromptPrefix + prompt
  135. if _, err := LLM(htmlPrompt, ch); err != nil {
  136. ch <- err.Error()
  137. close(ch)
  138. }
  139. }()
  140. response := ""
  141. for chunk := range ch {
  142. if _, err := fmt.Fprint(w, chunk); err != nil {
  143. return
  144. }
  145. response += chunk
  146. flusher.Flush()
  147. }
  148. fmt.Fprint(w, "</div>\n")
  149. finalHistory := history + fmt.Sprintf("Q: %s\nA: %s\n\n", query, response)
  150. fmt.Fprintf(w, htmlFooterTemplate, html.EscapeString(finalHistory))
  151. return
  152. }
  153. userAgent := r.Header.Get("User-Agent")
  154. isCurl := strings.Contains(userAgent, "curl") && !wantsHTML && !wantsJSON && !wantsStream
  155. if isCurl {
  156. w.Header().Set("Content-Type", "text/plain; charset=utf-8")
  157. w.Header().Set("Transfer-Encoding", "chunked")
  158. w.Header().Set("X-Accel-Buffering", "no")
  159. flusher := w.(http.Flusher)
  160. fmt.Fprintf(w, "Q: %s\nA: ", query)
  161. flusher.Flush()
  162. ch := make(chan string)
  163. go func() {
  164. if _, err := LLM(prompt, ch); err != nil {
  165. ch <- err.Error()
  166. close(ch)
  167. }
  168. }()
  169. response := ""
  170. for chunk := range ch {
  171. fmt.Fprint(w, chunk)
  172. response += chunk
  173. flusher.Flush()
  174. }
  175. fmt.Fprint(w, "\n")
  176. return
  177. }
  178. promptToUse := prompt
  179. if wantsHTML {
  180. promptToUse = htmlPromptPrefix + prompt
  181. }
  182. response, err := LLM(promptToUse, nil)
  183. if err != nil {
  184. content = err.Error()
  185. errJSON, _ := json.Marshal(map[string]string{"error": err.Error()})
  186. jsonResponse = string(errJSON)
  187. } else {
  188. respJSON, _ := json.Marshal(map[string]string{
  189. "question": query,
  190. "answer": response,
  191. })
  192. jsonResponse = string(respJSON)
  193. newExchange := fmt.Sprintf("Q: %s\nA: %s\n\n", query, response)
  194. if history != "" {
  195. content = history + newExchange
  196. } else {
  197. content = newExchange
  198. }
  199. if len(content) > 65536 {
  200. newExchangeLen := len(newExchange)
  201. if newExchangeLen > 65536 {
  202. content = newExchange[:65536]
  203. } else {
  204. maxHistory := 65536 - newExchangeLen
  205. if len(history) > maxHistory {
  206. content = history[len(history)-maxHistory:] + newExchange
  207. }
  208. }
  209. }
  210. }
  211. } else if history != "" {
  212. content = history
  213. }
  214. if wantsStream && query != "" {
  215. w.Header().Set("Content-Type", "text/event-stream")
  216. w.Header().Set("Cache-Control", "no-cache")
  217. w.Header().Set("Connection", "keep-alive")
  218. flusher, ok := w.(http.Flusher)
  219. if !ok {
  220. http.Error(w, "Streaming not supported", http.StatusInternalServerError)
  221. return
  222. }
  223. ch := make(chan string)
  224. go func() {
  225. if _, err := LLM(prompt, ch); err != nil {
  226. fmt.Fprintf(w, "data: Error: %s\n\n", err.Error())
  227. flusher.Flush()
  228. }
  229. }()
  230. for chunk := range ch {
  231. fmt.Fprintf(w, "data: %s\n\n", chunk)
  232. flusher.Flush()
  233. }
  234. fmt.Fprintf(w, "data: [DONE]\n\n")
  235. return
  236. }
  237. if wantsJSON && jsonResponse != "" {
  238. w.Header().Set("Content-Type", "application/json; charset=utf-8")
  239. fmt.Fprint(w, jsonResponse)
  240. } else if wantsHTML && query == "" {
  241. w.Header().Set("Content-Type", "text/html; charset=utf-8")
  242. fmt.Fprint(w, htmlHeader)
  243. parts := strings.Split("\n"+content, "\nQ: ")
  244. for _, part := range parts[1:] {
  245. if i := strings.Index(part, "\nA: "); i >= 0 {
  246. question := part[:i]
  247. answer := part[i+4:]
  248. answer = strings.TrimRight(answer, "\n")
  249. fmt.Fprintf(w, "<div class=\"q\">%s</div>\n", html.EscapeString(question))
  250. fmt.Fprintf(w, "<div class=\"a\">%s</div>\n", answer)
  251. }
  252. }
  253. fmt.Fprintf(w, htmlFooterTemplate, html.EscapeString(content))
  254. } else {
  255. w.Header().Set("Content-Type", "text/plain; charset=utf-8")
  256. fmt.Fprint(w, content)
  257. }
  258. }
  259. type ChatRequest struct {
  260. Model string `json:"model"`
  261. Messages []Message `json:"messages"`
  262. Stream bool `json:"stream,omitempty"`
  263. }
  264. type Message struct {
  265. Role string `json:"role"`
  266. Content string `json:"content"`
  267. }
  268. type ChatResponse struct {
  269. ID string `json:"id"`
  270. Object string `json:"object"`
  271. Created int64 `json:"created"`
  272. Model string `json:"model"`
  273. Choices []Choice `json:"choices"`
  274. }
  275. type Choice struct {
  276. Index int `json:"index"`
  277. Message Message `json:"message"`
  278. }
  279. func handleChatCompletions(w http.ResponseWriter, r *http.Request) {
  280. w.Header().Set("Access-Control-Allow-Origin", "*")
  281. if !rateLimitAllow(r.RemoteAddr) {
  282. http.Error(w, "Rate limit exceeded", http.StatusTooManyRequests)
  283. return
  284. }
  285. if r.Method != "POST" {
  286. w.Header().Set("Allow", "POST")
  287. http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
  288. return
  289. }
  290. var req ChatRequest
  291. if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
  292. http.Error(w, "Invalid JSON", http.StatusBadRequest)
  293. return
  294. }
  295. messages := make([]map[string]string, len(req.Messages))
  296. for i, msg := range req.Messages {
  297. messages[i] = map[string]string{
  298. "role": msg.Role,
  299. "content": msg.Content,
  300. }
  301. }
  302. if req.Stream {
  303. w.Header().Set("Content-Type", "text/event-stream")
  304. w.Header().Set("Cache-Control", "no-cache")
  305. w.Header().Set("Connection", "keep-alive")
  306. flusher, ok := w.(http.Flusher)
  307. if !ok {
  308. http.Error(w, "Streaming not supported", http.StatusInternalServerError)
  309. return
  310. }
  311. ch := make(chan string)
  312. go LLM(messages, ch)
  313. for chunk := range ch {
  314. resp := map[string]interface{}{
  315. "id": fmt.Sprintf("chatcmpl-%d", time.Now().Unix()),
  316. "object": "chat.completion.chunk",
  317. "created": time.Now().Unix(),
  318. "model": req.Model,
  319. "choices": []map[string]interface{}{{
  320. "index": 0,
  321. "delta": map[string]string{"content": chunk},
  322. }},
  323. }
  324. data, err := json.Marshal(resp)
  325. if err != nil {
  326. fmt.Fprintf(w, "data: Failed to marshal response\n\n")
  327. return
  328. }
  329. fmt.Fprintf(w, "data: %s\n\n", data)
  330. flusher.Flush()
  331. }
  332. fmt.Fprintf(w, "data: [DONE]\n\n")
  333. } else {
  334. response, err := LLM(messages, nil)
  335. if err != nil {
  336. http.Error(w, err.Error(), http.StatusInternalServerError)
  337. return
  338. }
  339. chatResp := ChatResponse{
  340. ID: fmt.Sprintf("chatcmpl-%d", time.Now().Unix()),
  341. Object: "chat.completion",
  342. Created: time.Now().Unix(),
  343. Model: req.Model,
  344. Choices: []Choice{{
  345. Index: 0,
  346. Message: Message{
  347. Role: "assistant",
  348. Content: response,
  349. },
  350. }},
  351. }
  352. w.Header().Set("Content-Type", "application/json")
  353. json.NewEncoder(w).Encode(chatResp)
  354. }
  355. }