{"id":284,"date":"2026-11-02T16:34:10","date_gmt":"2026-11-02T16:34:10","guid":{"rendered":"https:\/\/devdojo.co.in\/?p=284"},"modified":"2026-11-02T16:34:10","modified_gmt":"2026-11-02T16:34:10","slug":"genai-series-part-7-stream-llm-responses-chat-ui","status":"publish","type":"post","link":"https:\/\/devdojo.co.in\/index.php\/2026\/11\/02\/genai-series-part-7-stream-llm-responses-chat-ui\/","title":{"rendered":"Generative AI Part 7: How to Stream LLM Responses and Build a Simple Chat UI"},"content":{"rendered":"\n<div class=\"dd-post\">\n<style>\n.single article:has(.dd-hero) .entry-media{display:none}\n.dd-post{--navy:#0b1020;--blue:#3178c6;--violet:#7c5cff;--ink:#1f2433;--muted:#5b6275;--line:#e3e6ef;font-size:17px;line-height:1.75;color:var(--ink)}\n.dd-post h2{margin:2.2em 0 .6em;font-size:1.6em;line-height:1.3;scroll-margin-top:90px}\n.dd-post h3{margin:1.6em 0 .5em;font-size:1.22em;scroll-margin-top:90px}\n.dd-post a{color:var(--blue);text-decoration:underline;text-underline-offset:3px}\n.dd-post pre{background:#0f1430;color:#e6e9f5;padding:18px 20px;border-radius:12px;overflow-x:auto;line-height:1.6;border:1px solid #232a52;margin:1.2em 0;font-size:14.5px !important}\n.dd-post pre code{font-size:1em !important;background:none;color:inherit;padding:0;font-family:\"JetBrains Mono\",\"Fira Code\",Consolas,Menlo,monospace;white-space:pre}\n.dd-post :not(pre)>code{background:#eef1fb;color:#2b3a8f;padding:2px 6px;border-radius:5px;font-size:.9em}\n.dd-post .dd-file{display:inline-block;background:#151b3b;color:#c9d1ff;font:600 13px\/1 \"JetBrains Mono\",Consolas,monospace;padding:7px 12px;border-radius:8px 8px 0 0;margin:1em 0 -1.2em;position:relative}\n.dd-post .dd-hero{margin:0 0 1.6em;border-radius:16px;overflow:hidden;box-shadow:0 10px 30px rgba(11,16,32,.25)}\n.dd-post .dd-hero svg{display:block;width:100%;height:auto}\n.dd-post .dd-hero .node{transition:transform .25s,filter .25s;transform-box:fill-box;transform-origin:center;cursor:pointer}\n.dd-post .dd-hero .node:hover{transform:scale(1.08);filter:drop-shadow(0 0 10px #7c5cff)}\n.dd-post .dd-series{display:inline-flex;align-items:center;gap:8px;background:#151b3b;color:#dfe3ff;border-radius:999px;padding:6px 14px;font-size:.88em;font-weight:600;margin:0 0 .8em}\n.dd-post .dd-series span{background:var(--violet);color:#fff;border-radius:999px;padding:2px 10px}\n.dd-post .dd-prereq{font-size:.96em;color:var(--muted);border-left:3px solid var(--line);padding-left:12px;margin:.6em 0 1.2em}\n.dd-post .dd-summary{background:linear-gradient(135deg,#eef3ff,#f4efff);border:1px solid #d6dcf5;border-left:5px solid var(--violet);border-radius:12px;padding:18px 22px;margin:1.4em 0}\n.dd-post .dd-summary strong.lbl{display:block;font-size:1.05em;margin-bottom:6px;color:#3a2fa0}\n.dd-post .dd-summary ul{margin:.3em 0 0 1.1em}\n.dd-post .dd-toc{background:#f8f9fc;border:1px solid var(--line);border-radius:12px;padding:16px 22px;margin:1.4em 0}\n.dd-post .dd-toc ol{margin:.4em 0 0 1.2em;columns:2;column-gap:28px}\n.dd-post .dd-toc li{break-inside:avoid;margin:.15em 0}\n@media(max-width:640px){.dd-post .dd-toc ol{columns:1}}\n.dd-post .dd-tip,.dd-post .dd-note,.dd-post .dd-warn{border-radius:10px;padding:14px 18px;margin:1.3em 0}\n.dd-post .dd-tip{background:#ecfbf3;border-left:4px solid #1f9d61}\n.dd-post .dd-note{background:#eef4ff;border-left:4px solid var(--blue)}\n.dd-post .dd-warn{background:#fff6e8;border-left:4px solid #e08a00}\n.dd-post .dd-cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:14px;margin:1.3em 0}\n.dd-post .dd-card{border:1px solid var(--line);border-radius:12px;padding:14px 16px;background:#fff;box-shadow:0 2px 8px rgba(20,30,70,.05)}\n.dd-post .dd-card b{display:block;color:#3a2fa0;margin-bottom:4px}\n.dd-post table{width:100%;border-collapse:collapse;margin:1.3em 0;font-size:.95em}\n.dd-post th,.dd-post td{border:1px solid var(--line);padding:10px 12px;text-align:left;vertical-align:top}\n.dd-post th{background:#151b3b;color:#fff}\n.dd-post tr:nth-child(even) td{background:#f8f9fc}\n.dd-post .dd-tablewrap{overflow-x:auto}\n.dd-post figure{margin:1.6em 0;text-align:center}\n.dd-post figure svg{width:100%;height:auto;border-radius:12px}\n.dd-post figcaption{font-size:.9em;color:var(--muted);margin-top:8px}\n.dd-post details{border:1px solid var(--line);border-radius:10px;padding:12px 16px;margin:.7em 0;background:#fff}\n.dd-post details summary{cursor:pointer;font-weight:600}\n.dd-post details[open] summary{margin-bottom:8px;color:#3a2fa0}\n.dd-post .dd-roadmap{border:2px solid #d6dcf5;background:#fbfbff}\n.dd-post .dd-roadmap h4{margin:1em 0 .3em;color:#151b3b;font-size:1em;text-transform:uppercase;letter-spacing:.04em}\n.dd-post .dd-roadmap ol{margin:.2em 0 .6em 1.4em;padding:0}\n.dd-post .dd-roadmap li{margin:.2em 0}\n.dd-post .dd-roadmap .here{background:var(--violet);color:#fff;border-radius:999px;padding:1px 9px;font-size:.8em;font-weight:700;margin-left:6px}\n.dd-post .dd-roadmap .soon{color:#8a90a6;font-size:.82em;margin-left:6px}\n.dd-post .dd-tabs{border:1px solid var(--line);border-radius:12px;padding:6px 16px 16px;margin:1.4em 0;background:#fbfbfe}\n.dd-post .dd-tabs>input{position:absolute;opacity:0;pointer-events:none}\n.dd-post .dd-tabs .labels label{display:inline-block;padding:8px 16px;margin:10px 6px 0 0;border-radius:999px;border:1px solid #cfd5ea;cursor:pointer;font-weight:600;font-size:.95em;background:#fff;transition:background .2s}\n.dd-post .dd-tabs .labels label:hover{border-color:var(--violet)}\n.dd-post .dd-tabs .panel{display:none;margin-top:12px}\n.dd-post .dd-tabs #ti1:checked~.labels label[for=ti1],.dd-post .dd-tabs #ti2:checked~.labels label[for=ti2],.dd-post .dd-tabs #ti3:checked~.labels label[for=ti3],.dd-post .dd-tabs #ti4:checked~.labels label[for=ti4]{background:var(--violet);color:#fff;border-color:var(--violet)}\n.dd-post .dd-tabs #ti1:checked~.p1,.dd-post .dd-tabs #ti2:checked~.p2,.dd-post .dd-tabs #ti3:checked~.p3,.dd-post .dd-tabs #ti4:checked~.p4{display:block}\n.dd-post .dd-tabs #ti1:focus-visible~.labels label[for=ti1],.dd-post .dd-tabs #ti2:focus-visible~.labels label[for=ti2],.dd-post .dd-tabs #ti3:focus-visible~.labels label[for=ti3],.dd-post .dd-tabs #ti4:focus-visible~.labels label[for=ti4]{outline:2px solid var(--blue);outline-offset:2px}\n.dd-post .dd-cta{background:linear-gradient(135deg,#0b1020,#151b3b);color:#e8ebff;border-radius:16px;padding:24px 26px;margin:2em 0;text-align:center}\n.dd-post .dd-cta h3{color:#fff;margin-top:0}\n.dd-post .dd-cta a{color:#a99bff}\n.dd-post .dd-vsc{position:relative;margin:1.5em 0;border-radius:12px;overflow:hidden;background:#1e1e1e;border:1px solid #2b2b2b;box-shadow:0 14px 34px rgba(0,0,0,.28);font-family:\"Segoe UI\",system-ui,sans-serif}\n.dd-post .dd-vsc>input{position:absolute;opacity:0;pointer-events:none}\n.dd-post .dd-vsc-bar{display:flex;align-items:center;gap:12px;background:#323233;color:#cccccc;font-size:12.5px;padding:8px 12px}\n.dd-post .dd-dots{display:inline-flex;gap:7px}.dd-post .dd-dots i{width:12px;height:12px;border-radius:50%;background:#ff5f57;display:block}.dd-post .dd-dots i:nth-child(2){background:#febc2e}.dd-post .dd-dots i:nth-child(3){background:#28c840}\n.dd-post .dd-vsc-title{flex:1;text-align:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;padding-right:50px;opacity:.85}\n.dd-post .dd-vsc-tabs{display:flex;align-items:stretch;background:#252526;border-bottom:1px solid #1e1e1e;overflow-x:auto}\n.dd-post .dd-vsc-tabs label{display:flex;align-items:center;gap:7px;padding:9px 16px;color:#969696;font-size:13px;cursor:pointer;background:#2d2d2d;border-right:1px solid #252526;border-top:1px solid transparent;white-space:nowrap;margin:0;transition:color .15s}\n.dd-post .dd-vsc-tabs label:hover{color:#e0e0e0}\n.dd-post .dd-vsc-tabs .ic{font:700 9.5px\/1 Consolas,monospace;padding:3px 4px;border-radius:3px;color:#1e1e1e}\n.dd-post .ic.py{background:#ffd43b}.dd-post .ic.js{background:#f1e05a}.dd-post .ic.sh{background:#89e051}\n.dd-post .dd-vsc-tabs label.run{margin-left:auto;background:transparent;border:0;color:#89d185;font-weight:600;display:none}\n.dd-post .dd-vsc-tabs label.run:hover{color:#b5f0b0;background:#2a2d2e}\n.dd-post .dd-vsc-pane{display:none}\n.dd-post .dd-vsc>.f1:checked~.dd-vsc-tabs .t1,.dd-post .dd-vsc>.f2:checked~.dd-vsc-tabs .t2,.dd-post .dd-vsc>.f3:checked~.dd-vsc-tabs .t3{background:#1e1e1e;color:#ffffff;border-top-color:#3178c6}\n.dd-post .dd-vsc>.f1:checked~.p1,.dd-post .dd-vsc>.f2:checked~.p2,.dd-post .dd-vsc>.f3:checked~.p3{display:block}\n.dd-post .dd-vsc>.f1:checked~.dd-vsc-tabs .run1,.dd-post .dd-vsc>.f2:checked~.dd-vsc-tabs .run2,.dd-post .dd-vsc>.f3:checked~.dd-vsc-tabs .run3{display:flex}\n.dd-post .dd-vsc>.f1:focus-visible~.dd-vsc-tabs .t1,.dd-post .dd-vsc>.f2:focus-visible~.dd-vsc-tabs .t2,.dd-post .dd-vsc>.f3:focus-visible~.dd-vsc-tabs .t3,.dd-post .dd-vsc>input[type=checkbox]:focus-visible~.dd-vsc-tabs .run{outline:2px solid #3178c6;outline-offset:-2px}\n.dd-post .dd-vsc-crumbs{background:#1e1e1e;color:#a9a9a9;font-size:12px;padding:5px 16px 2px}\n.dd-post .dd-vsc pre.dd-vsc-code{background:#1e1e1e;color:#d4d4d4;border:0;border-radius:0;margin:0;padding:8px 0 14px;counter-reset:ln;line-height:1.62;overflow-x:auto}\n.dd-post .dd-vsc pre code{font-family:Consolas,\"JetBrains Mono\",\"Fira Code\",Menlo,monospace;color:inherit;background:none;white-space:pre}\n.dd-post .dd-vsc .l{display:inline-block;min-width:100%;padding-right:18px;box-sizing:border-box}\n.dd-post .dd-vsc .l::before{counter-increment:ln;content:counter(ln);display:inline-block;width:3.2em;padding-right:1.4em;text-align:right;color:#6e7681;user-select:none;-webkit-user-select:none}\n.dd-post .dd-vsc .l:hover{background:#2a2d2e}.dd-post .dd-vsc .l:hover::before{color:#c6c6c6}\n.dd-post .dd-vsc .k,.dd-post .dd-vsc .kn{color:#c586c0}.dd-post .dd-vsc .kc{color:#569cd6}.dd-post .dd-vsc .s{color:#ce9178}.dd-post .dd-vsc .c{color:#6a9955}.dd-post .dd-vsc .m{color:#b5cea8}.dd-post .dd-vsc .nf{color:#dcdcaa}.dd-post .dd-vsc .nc{color:#4ec9b0}.dd-post .dd-vsc .nb{color:#4ec9b0}.dd-post .dd-vsc .nd{color:#dcdcaa}.dd-post .dd-vsc .n{color:#9cdcfe}.dd-post .dd-vsc .o{color:#d4d4d4}.dd-post .dd-vsc .gp{color:#89d185;user-select:none;-webkit-user-select:none}\n.dd-post .dd-vsc-term{display:none;border-top:1px solid #3c3c3c;background:#181818}\n.dd-post .dd-vsc>.r1:checked~.p1 .dd-vsc-term,.dd-post .dd-vsc>.r2:checked~.p2 .dd-vsc-term,.dd-post .dd-vsc>.r3:checked~.p3 .dd-vsc-term{display:block;animation:ddterm .35s ease-out}\n@keyframes ddterm{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}\n.dd-post .dd-term-head{display:flex;gap:18px;align-items:center;font-size:11px;letter-spacing:.06em;color:#8b8b8b;padding:7px 16px 0}\n.dd-post .dd-term-head span:first-child{color:#e7e7e7;border-bottom:1px solid #e7e7e7;padding-bottom:4px}\n.dd-post .dd-term-head .dd-term-note{margin-left:auto;letter-spacing:0;text-transform:none;color:#d7ba7d;border:0;font-size:11.5px}\n.dd-post .dd-vsc-term pre{background:#181818;color:#cccccc;border:0;margin:0;padding:10px 16px 14px;border-radius:0}\n.dd-post .dd-vsc-status{display:flex;flex-wrap:wrap;gap:4px 14px;background:#007acc;color:#fff;font-size:11.5px;padding:4px 12px}\n.dd-post .dd-vsc-status .sp{flex:1}\n.dd-post .dd-vsc-termonly pre.dd-vsc-code{background:#181818;padding:12px 16px}\n@media(max-width:640px){.dd-post .dd-vsc-title{padding-right:0;text-align:left}.dd-post .dd-vsc-status span:nth-child(n+4){display:none}.dd-post .dd-vsc .l::before{width:2.2em;padding-right:1em}}\n.dd-post .dd-vsc-hint{font-size:.93em;color:var(--muted);margin-top:-.6em}\n.dd-post .dd-next{border:1px dashed var(--violet);border-radius:12px;padding:16px 20px;margin:1.6em 0;background:#f7f4ff}\n<\/style>\n<style>\n.dd-post .ic.html{background:#e44d26;color:#fff !important}\n.dd-post .dd-steps{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px;margin:.4em 0}\n.dd-post .dd-steps div{background:#151b3b;color:#e8ebff;border-radius:10px;padding:10px 12px;font-size:.92em}\n.dd-post .dd-steps b{display:block;color:#a99bff;font-size:.82em;text-transform:uppercase;letter-spacing:.05em}\n.dd-post .dd-steps code{background:#262f63 !important;color:#fff !important}\n<\/style>\n\n<div class=\"dd-hero\">\n<svg viewBox=\"0 0 1200 520\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" role=\"img\" aria-label=\"Generative AI series Part 7 banner: a chat window in the browser receives an answer word by word from a Node.js or Python server, which streams tokens from an LLM such as OpenAI or a local Ollama model\">\n<defs>\n<linearGradient id=\"ddbg\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\"><stop offset=\"0\" stop-color=\"#0b1020\"\/><stop offset=\"1\" stop-color=\"#151b3b\"\/><\/linearGradient>\n<linearGradient id=\"ddac\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"0\"><stop offset=\"0\" stop-color=\"#3178c6\"\/><stop offset=\"1\" stop-color=\"#7c5cff\"\/><\/linearGradient>\n<radialGradient id=\"ddglow\" cx=\".5\" cy=\".5\" r=\".5\"><stop offset=\"0\" stop-color=\"#7c5cff\" stop-opacity=\".45\"\/><stop offset=\"1\" stop-color=\"#7c5cff\" stop-opacity=\"0\"\/><\/radialGradient>\n<\/defs>\n<rect x=\"0\" y=\"-60\" width=\"1200\" height=\"640\" fill=\"url(#ddbg)\"\/>\n<circle cx=\"1000\" cy=\"110\" r=\"250\" fill=\"url(#ddglow)\"\/>\n<g opacity=\".12\" stroke=\"#9fb2ff\"><path d=\"M0 100H1200M0 200H1200M0 300H1200M0 400H1200M200 0V520M400 0V520M600 0V520M800 0V520M1000 0V520\"\/><\/g>\n<rect x=\"60\" y=\"46\" width=\"128\" height=\"38\" rx=\"19\" fill=\"url(#ddac)\"\/>\n<text x=\"124\" y=\"71\" text-anchor=\"middle\" font-family=\"Segoe UI,Arial,sans-serif\" font-size=\"18\" font-weight=\"700\" fill=\"#fff\">PART 7<\/text>\n<text x=\"204\" y=\"71\" font-family=\"Segoe UI,Arial,sans-serif\" font-size=\"18\" fill=\"#aab4e8\">Generative AI Series \u00b7 DevDojo<\/text>\n<text x=\"60\" y=\"148\" font-family=\"Segoe UI,Arial,sans-serif\" font-size=\"54\" font-weight=\"800\" fill=\"#ffffff\">Stream LLM Responses<\/text>\n<text x=\"60\" y=\"206\" font-family=\"Segoe UI,Arial,sans-serif\" font-size=\"44\" font-weight=\"800\" fill=\"url(#ddac)\">and Build a Chat UI<\/text>\n<text x=\"60\" y=\"248\" font-family=\"Segoe UI,Arial,sans-serif\" font-size=\"23\" fill=\"#c9d1ff\">Show answers word by word, like ChatGPT<\/text>\n<g font-family=\"Segoe UI,Arial,sans-serif\">\n<g class=\"node\"><rect x=\"60\" y=\"290\" width=\"300\" height=\"190\" rx=\"16\" fill=\"#1b2250\" stroke=\"#61dafb\" stroke-width=\"2\"\/><circle cx=\"84\" cy=\"312\" r=\"6\" fill=\"#ff5f57\"\/><circle cx=\"102\" cy=\"312\" r=\"6\" fill=\"#febc2e\"\/><circle cx=\"120\" cy=\"312\" r=\"6\" fill=\"#28c840\"\/><text x=\"210\" y=\"317\" text-anchor=\"middle\" font-size=\"15\" fill=\"#aab4e8\">Chat UI<\/text><rect x=\"150\" y=\"334\" width=\"190\" height=\"34\" rx=\"10\" fill=\"#3178c6\"\/><text x=\"245\" y=\"357\" text-anchor=\"middle\" font-size=\"15\" fill=\"#fff\">What is streaming?<\/text><rect x=\"78\" y=\"380\" width=\"230\" height=\"78\" rx=\"10\" fill=\"#0b1020\"\/><text x=\"92\" y=\"405\" font-size=\"15\" fill=\"#e8ebff\">Streaming sends the<\/text><text x=\"92\" y=\"428\" font-size=\"15\" fill=\"#e8ebff\">answer piece by<\/text><rect x=\"214\" y=\"415\" width=\"10\" height=\"18\" fill=\"#7c5cff\"><animate attributeName=\"opacity\" values=\"1;0;1\" dur=\"1s\" repeatCount=\"indefinite\"\/><\/rect><\/g>\n<g class=\"node\"><rect x=\"490\" y=\"335\" width=\"220\" height=\"100\" rx=\"20\" fill=\"url(#ddac)\"\/><text x=\"600\" y=\"377\" text-anchor=\"middle\" font-size=\"27\" font-weight=\"800\" fill=\"#fff\">Your server<\/text><text x=\"600\" y=\"408\" text-anchor=\"middle\" font-size=\"16\" fill=\"#eef\">Node.js \u00b7 FastAPI<\/text><\/g>\n<g class=\"node\"><rect x=\"840\" y=\"300\" width=\"180\" height=\"56\" rx=\"12\" fill=\"#0b1020\" stroke=\"#7c5cff\" stroke-width=\"2\"\/><text x=\"930\" y=\"335\" text-anchor=\"middle\" font-size=\"19\" font-weight=\"700\" fill=\"#fff\">OpenAI API<\/text><\/g>\n<g class=\"node\"><rect x=\"840\" y=\"410\" width=\"180\" height=\"56\" rx=\"12\" fill=\"#0b1020\" stroke=\"#89e051\" stroke-width=\"2\"\/><text x=\"930\" y=\"445\" text-anchor=\"middle\" font-size=\"19\" font-weight=\"700\" fill=\"#fff\">Ollama (local)<\/text><\/g>\n<g class=\"node\"><rect x=\"1060\" y=\"345\" width=\"110\" height=\"80\" rx=\"16\" fill=\"#0b1020\" stroke=\"#3178c6\" stroke-width=\"2\"\/><text x=\"1115\" y=\"380\" text-anchor=\"middle\" font-size=\"18\" font-weight=\"800\" fill=\"#fff\">LLM<\/text><text x=\"1115\" y=\"404\" text-anchor=\"middle\" font-size=\"13\" fill=\"#c9d1ff\">token by token<\/text><\/g>\n<\/g>\n<path id=\"ddf1\" d=\"M840 328 C 770 328, 770 385, 710 385\" fill=\"none\" stroke=\"url(#ddac)\" stroke-width=\"3\" stroke-dasharray=\"8 8\" opacity=\".8\"\/>\n<path id=\"ddf2\" d=\"M840 438 C 770 438, 770 395, 710 395\" fill=\"none\" stroke=\"url(#ddac)\" stroke-width=\"3\" stroke-dasharray=\"8 8\" opacity=\".8\"\/>\n<path id=\"ddf3\" d=\"M490 390 C 430 390, 420 420, 360 420\" fill=\"none\" stroke=\"url(#ddac)\" stroke-width=\"3\" stroke-dasharray=\"8 8\" opacity=\".8\"\/>\n<path id=\"ddf4\" d=\"M1060 385 C 1040 385, 1040 335, 1020 330\" fill=\"none\" stroke=\"url(#ddac)\" stroke-width=\"3\" stroke-dasharray=\"8 8\" opacity=\".8\"\/>\n<path id=\"ddf5\" d=\"M1060 390 C 1040 390, 1040 435, 1020 438\" fill=\"none\" stroke=\"url(#ddac)\" stroke-width=\"3\" stroke-dasharray=\"8 8\" opacity=\".8\"\/>\n<circle r=\"6\" fill=\"#7c5cff\"><animateMotion dur=\"1.6s\" repeatCount=\"indefinite\"><mpath href=\"#ddf1\"\/><\/animateMotion><\/circle>\n<circle r=\"6\" fill=\"#89e051\"><animateMotion dur=\"1.6s\" begin=\".8s\" repeatCount=\"indefinite\"><mpath href=\"#ddf2\"\/><\/animateMotion><\/circle>\n<circle r=\"6\" fill=\"#61dafb\"><animateMotion dur=\"1.2s\" repeatCount=\"indefinite\"><mpath href=\"#ddf3\"\/><\/animateMotion><\/circle>\n<circle r=\"5\" fill=\"#ffffff\"><animateMotion dur=\"1.2s\" begin=\".4s\" repeatCount=\"indefinite\"><mpath href=\"#ddf3\"\/><\/animateMotion><\/circle>\n<circle r=\"5\" fill=\"#ffffff\"><animateMotion dur=\"1.4s\" repeatCount=\"indefinite\"><mpath href=\"#ddf4\"\/><\/animateMotion><\/circle>\n<circle r=\"5\" fill=\"#ffffff\"><animateMotion dur=\"1.4s\" begin=\".7s\" repeatCount=\"indefinite\"><mpath href=\"#ddf5\"\/><\/animateMotion><\/circle>\n<text x=\"60\" y=\"510\" font-family=\"Segoe UI,Arial,sans-serif\" font-size=\"16\" fill=\"#7f89b5\">devdojo.co.in \u00b7 Beginner friendly \u00b7 Python + Node.js + browser examples<\/text>\n<\/svg>\n<\/div>\n\n<div class=\"dd-series\">Series: Generative AI <span>Part 7 of 30<\/span><\/div>\n<p class=\"dd-prereq\"><strong>What you should know first:<\/strong> read <a href=\"https:\/\/devdojo.co.in\/?p=281\">Part 6: Run LLMs Locally with Ollama<\/a> and <a href=\"https:\/\/devdojo.co.in\/?p=223\">Part 3: Your First LLM API Calls<\/a>. You need Node.js 20.6+ or Python 3.10+, and either a free local model in Ollama or an OpenAI API key.<\/p>\n\n<p>When you ask ChatGPT a question, the answer does not appear all at once. It types itself out, word by word. That trick is called <strong>streaming<\/strong>, and it is the single biggest thing that makes an AI app feel fast. In this part you will learn how to <strong>stream LLM responses<\/strong> in Python and Node.js, look at the raw Server-Sent Events that travel over the wire, build a terminal chatbot that remembers the conversation and then build a <strong>simple chat UI<\/strong> in the browser with a Stop button. Every example works with a free local model through Ollama or with a cloud model such as OpenAI, by changing just one setting.<\/p>\n\n<div class=\"dd-summary\">\n<strong class=\"lbl\">Quick Summary<\/strong>\n<ul>\n<li><strong>Streaming<\/strong> means the model sends its answer in small pieces while it is still writing, instead of one big reply at the end.<\/li>\n<li>Add <code>stream: true<\/code> (or <code>stream=True<\/code>) to the API call and loop over the events or chunks that come back.<\/li>\n<li>Under the hood the API uses <strong>Server-Sent Events<\/strong>: many <code>data: {\u2026}<\/code> lines on one open HTTP connection.<\/li>\n<li>For a web app, the <strong>server<\/strong> calls the LLM (it keeps the API key) and forwards each piece; the <strong>browser<\/strong> reads them with <code>fetch<\/code> + <code>getReader()<\/code>.<\/li>\n<li>The model has no memory, so the app keeps a <code>messages<\/code> list and sends it with every question.<\/li>\n<li>Use an <code>AbortController<\/code> to stop an answer and save tokens.<\/li>\n<\/ul>\n<\/div>\n\n<nav class=\"dd-toc\" aria-label=\"Table of contents\">\n<strong>In this post<\/strong>\n<ol>\n<li><a href=\"#what-is-streaming\">What is streaming?<\/a><\/li>\n<li><a href=\"#why-stream\">Why stream LLM responses?<\/a><\/li>\n<li><a href=\"#first-stream\">Your first streamed answer<\/a><\/li>\n<li><a href=\"#sse\">What travels over the wire (SSE)<\/a><\/li>\n<li><a href=\"#terminal-chat\">A terminal chatbot with memory<\/a><\/li>\n<li><a href=\"#chat-ui\">Build a simple chat UI<\/a><\/li>\n<li><a href=\"#fastapi\">The same server in FastAPI<\/a><\/li>\n<li><a href=\"#stop-button\">Stopping an answer<\/a><\/li>\n<li><a href=\"#errors\">Common errors and fixes<\/a><\/li>\n<li><a href=\"#best-practices\">Best practices<\/a><\/li>\n<li><a href=\"#practice\">Practice task<\/a><\/li>\n<li><a href=\"#faq\">FAQ<\/a><\/li>\n<\/ol>\n<\/nav>\n\n<details class=\"dd-roadmap\">\n<summary>Full Generative AI roadmap (30 parts)<\/summary>\n<h4>Beginner<\/h4>\n<ol start=\"1\">\n<li><a href=\"https:\/\/devdojo.co.in\/?p=215\">What Is Generative AI and How Do LLMs Work?<\/a><\/li>\n<li><a href=\"https:\/\/devdojo.co.in\/?p=219\">Tokens, Context Windows and Model Parameters<\/a><\/li>\n<li><a href=\"https:\/\/devdojo.co.in\/?p=223\">Your First LLM API Calls in Python and Node.js (OpenAI, Anthropic, Gemini)<\/a><\/li>\n<li><a href=\"https:\/\/devdojo.co.in\/?p=240\">Prompt Engineering Basics: Roles, Instructions and Examples<\/a><\/li>\n<li><a href=\"https:\/\/devdojo.co.in\/?p=244\">Temperature, Top-p and Controlling Model Output<\/a><\/li>\n<li><a href=\"https:\/\/devdojo.co.in\/?p=281\">Run LLMs Locally with Ollama<\/a><\/li>\n<li>Streaming Responses and Building a Simple Chat UI <span class=\"here\">You are here<\/span><\/li>\n<li>Structured Outputs: Getting Reliable JSON from LLMs <span class=\"soon\">Coming soon<\/span><\/li>\n<\/ol>\n<h4>Intermediate<\/h4>\n<ol start=\"9\">\n<li>Embeddings Explained: Turning Text into Vectors <span class=\"soon\">Coming soon<\/span><\/li>\n<li>Vector Databases: pgvector, Chroma and Pinecone <span class=\"soon\">Coming soon<\/span><\/li>\n<li>RAG Fundamentals: Retrieval-Augmented Generation <span class=\"soon\">Coming soon<\/span><\/li>\n<li>Document Loading and Chunking Strategies <span class=\"soon\">Coming soon<\/span><\/li>\n<li>Better RAG: Hybrid Search, Re-ranking and Query Rewriting <span class=\"soon\">Coming soon<\/span><\/li>\n<li>Function Calling and Tool Use <span class=\"soon\">Coming soon<\/span><\/li>\n<li>Conversation Memory and Long Context Management <span class=\"soon\">Coming soon<\/span><\/li>\n<li>Multimodal AI: Vision, Images and Audio Inputs <span class=\"soon\">Coming soon<\/span><\/li>\n<li>How Image Generation Works: Diffusion Models Basics <span class=\"soon\">Coming soon<\/span><\/li>\n<li>LangChain vs LlamaIndex: When to Use a Framework <span class=\"soon\">Coming soon<\/span><\/li>\n<li>Testing LLM Apps: Evals, Metrics and Test Sets <span class=\"soon\">Coming soon<\/span><\/li>\n<li>Cutting Cost and Latency: Caching, Batching and Model Choice <span class=\"soon\">Coming soon<\/span><\/li>\n<\/ol>\n<h4>Advanced<\/h4>\n<ol start=\"21\">\n<li>Fine-Tuning vs Prompting vs RAG: Choosing the Right Tool <span class=\"soon\">Coming soon<\/span><\/li>\n<li>Fine-Tuning an Open Model with LoRA <span class=\"soon\">Coming soon<\/span><\/li>\n<li>Transformers Deep Dive: Attention Explained Simply <span class=\"soon\">Coming soon<\/span><\/li>\n<li>Reasoning Models and Thinking Time <span class=\"soon\">Coming soon<\/span><\/li>\n<li>Guardrails, Safety and Prompt Injection Defence <span class=\"soon\">Coming soon<\/span><\/li>\n<li>Monitoring and Tracing LLM Apps in Production <span class=\"soon\">Coming soon<\/span><\/li>\n<li>Serving Open Models: vLLM, Quantization and GPUs <span class=\"soon\">Coming soon<\/span><\/li>\n<li>Building a Production GenAI App End to End <span class=\"soon\">Coming soon<\/span><\/li>\n<li>Responsible AI: Privacy, Licensing and Bias <span class=\"soon\">Coming soon<\/span><\/li>\n<li>Final Project: Build Your Own AI Knowledge Assistant <span class=\"soon\">Coming soon<\/span><\/li>\n<\/ol>\n<\/details>\n<\/div>\n\n\n\n<div class=\"dd-post\">\n<h2 id=\"what-is-streaming\">What is streaming?<\/h2>\n<p>An LLM writes its answer one token at a time (you learned about tokens in <a href=\"https:\/\/devdojo.co.in\/?p=219\">Part 2<\/a>). In a normal API call, the provider waits until the very last token is ready, then sends the complete answer in one response. With <strong>streaming<\/strong>, the provider sends each new token, or a small group of tokens, the moment it exists. Your code receives a stream of tiny pieces and can show them right away.<\/p>\n<p>Think of ordering food. A normal call is like waiting at the counter until the whole meal is packed. Streaming is like a restaurant that serves each dish as soon as it leaves the kitchen: the total time is similar, but you start eating much sooner.<\/p>\n\n<figure>\n<svg viewBox=\"0 0 900 340\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" role=\"img\" aria-label=\"Timeline comparing a normal LLM call, where the user waits about 6 seconds and then sees the whole answer, with a streamed call, where the first words appear after about half a second and keep arriving until the answer is done\">\n<rect width=\"900\" height=\"340\" fill=\"#0b1020\"\/>\n<g font-family=\"Segoe UI,Arial,sans-serif\">\n<text x=\"30\" y=\"40\" font-size=\"20\" font-weight=\"700\" fill=\"#fff\">Same answer, very different feeling<\/text>\n<text x=\"30\" y=\"96\" font-size=\"16\" font-weight=\"700\" fill=\"#c9d1ff\">Without streaming<\/text>\n<rect x=\"200\" y=\"78\" width=\"560\" height=\"28\" rx=\"8\" fill=\"#232a52\"\/>\n<text x=\"480\" y=\"98\" text-anchor=\"middle\" font-size=\"14\" fill=\"#8a93c4\">waiting\u2026 blank screen<\/text>\n<rect x=\"760\" y=\"78\" width=\"110\" height=\"28\" rx=\"8\" fill=\"#3178c6\"\/>\n<text x=\"815\" y=\"98\" text-anchor=\"middle\" font-size=\"14\" fill=\"#fff\">full answer<\/text>\n<text x=\"30\" y=\"186\" font-size=\"16\" font-weight=\"700\" fill=\"#c9d1ff\">With streaming<\/text>\n<rect x=\"200\" y=\"168\" width=\"40\" height=\"28\" rx=\"8\" fill=\"#232a52\"\/>\n<g fill=\"#7c5cff\"><rect x=\"244\" y=\"168\" width=\"54\" height=\"28\" rx=\"6\"\/><rect x=\"302\" y=\"168\" width=\"70\" height=\"28\" rx=\"6\"\/><rect x=\"376\" y=\"168\" width=\"44\" height=\"28\" rx=\"6\"\/><rect x=\"424\" y=\"168\" width=\"80\" height=\"28\" rx=\"6\"\/><rect x=\"508\" y=\"168\" width=\"60\" height=\"28\" rx=\"6\"\/><rect x=\"572\" y=\"168\" width=\"74\" height=\"28\" rx=\"6\"\/><rect x=\"650\" y=\"168\" width=\"52\" height=\"28\" rx=\"6\"\/><rect x=\"706\" y=\"168\" width=\"70\" height=\"28\" rx=\"6\"\/><rect x=\"780\" y=\"168\" width=\"90\" height=\"28\" rx=\"6\"\/><\/g>\n<text x=\"250\" y=\"226\" font-size=\"14\" fill=\"#89e051\">first words after ~0.5 s<\/text>\n<path d=\"M244 202 V212\" stroke=\"#89e051\" stroke-width=\"2\"\/>\n<line x1=\"200\" y1=\"270\" x2=\"870\" y2=\"270\" stroke=\"#5b6694\" stroke-width=\"2\"\/>\n<g font-size=\"13\" fill=\"#8a93c4\"><text x=\"200\" y=\"292\">0 s<\/text><text x=\"425\" y=\"292\">2 s<\/text><text x=\"650\" y=\"292\">4 s<\/text><text x=\"845\" y=\"292\">6 s<\/text><\/g>\n<text x=\"30\" y=\"324\" font-size=\"14\" fill=\"#aab4e8\">Total time is the same. Streaming only changes when the user sees the first words.<\/text>\n<\/g>\n<\/svg>\n<figcaption>Figure 1: A normal call shows nothing until the whole answer is ready. A streamed call shows the first words almost at once.<\/figcaption>\n<\/figure>\n\n<h2 id=\"why-stream\">Why stream LLM responses?<\/h2>\n<div class=\"dd-cards\">\n<div class=\"dd-card\"><b>Feels much faster<\/b>Users see the first words in well under a second instead of staring at a spinner for 5\u201320 seconds.<\/div>\n<div class=\"dd-card\"><b>Users can stop early<\/b>If the answer goes in the wrong direction, a Stop button cancels it and you stop paying for extra tokens.<\/div>\n<div class=\"dd-card\"><b>Fewer timeouts<\/b>Long answers keep the connection busy, so proxies and serverless platforms are less likely to cut a silent request.<\/div>\n<div class=\"dd-card\"><b>Same price<\/b>Streaming does not cost more. You pay for the same input and output tokens either way.<\/div>\n<\/div>\n<p>The one thing to remember: streaming improves <em>time to first token<\/em> (how quickly something appears), not the total time. If you need the full answer before doing anything, for example to parse JSON, a normal call is often simpler. We will cover reliable JSON in the next part.<\/p>\n\n<h2 id=\"first-stream\">Your first streamed answer in Python and Node.js<\/h2>\n<p>Let us start with the cloud example from Part 3 and make it stream. Set up a folder and install the packages first. If you followed Part 3 you already have most of this.<\/p>\n<div class=\"dd-vsc dd-vsc-termonly\" role=\"group\" aria-label=\"Terminal: Terminal \u2014 set up the project\">\n<div class=\"dd-vsc-bar\"><span class=\"dd-dots\"><i><\/i><i><\/i><i><\/i><\/span><span class=\"dd-vsc-title\">Terminal \u2014 set up the project<\/span><\/div>\n<pre class=\"dd-vsc-code\"><code><span class=\"l\"><span class=\"c\"># 1. project folder (Node.js 20.6+ and Python 3.10+)<\/span><\/span>\n<span class=\"l\"><span class=\"gp\">$<\/span> mkdir devdojo-chat &amp;&amp; cd devdojo-chat &amp;&amp; mkdir public<\/span>\n<span class=\"l\"><span class=\"gp\">$<\/span> npm init -y &amp;&amp; npm pkg set type=module<\/span>\n<span class=\"l\"><span class=\"gp\">$<\/span> npm install openai<\/span>\n<span class=\"l\"><span class=\"gp\">$<\/span> pip install openai fastapi uvicorn python-dotenv<\/span>\n<span class=\"l\"><span class=\"c\"># 2a. free and local: make sure Ollama is running (Part 6)<\/span><\/span>\n<span class=\"l\"><span class=\"gp\">$<\/span> ollama pull llama3.2<\/span>\n<span class=\"l\"><span class=\"c\"># 2b. or cloud: put these lines in a .env file instead<\/span><\/span>\n<span class=\"l\"><span class=\"c\"># LLM_BASE_URL=https:\/\/api.openai.com\/v1<\/span><\/span>\n<span class=\"l\"><span class=\"c\"># LLM_API_KEY=sk-your-openai-key<\/span><\/span>\n<span class=\"l\"><span class=\"c\"># LLM_MODEL=gpt-6-luna<\/span><\/span><\/code><\/pre>\n<\/div>\n<p>Now the code. Compared with Part 3 there is only one new line, <code>stream: true<\/code>. Instead of one response object you get an <strong>iterator of events<\/strong>. Each event has a <code>type<\/code>. The two you care about most are <code>response.output_text.delta<\/code> (a new piece of text in <code>event.delta<\/code>) and <code>response.completed<\/code> (the end, with token usage).<\/p>\n<div class=\"dd-vsc\" role=\"group\" aria-label=\"Code editor: stream_basic.py, stream-basic.mjs\">\n<input type=\"radio\" name=\"v1\" id=\"v1f1\" class=\"f1\" checked>\n<input type=\"radio\" name=\"v1\" id=\"v1f2\" class=\"f2\">\n<input type=\"checkbox\" id=\"v1r1\" class=\"r1\">\n<input type=\"checkbox\" id=\"v1r2\" class=\"r2\">\n<div class=\"dd-vsc-bar\"><span class=\"dd-dots\"><i><\/i><i><\/i><i><\/i><\/span><span class=\"dd-vsc-title\">stream_basic.py \u2014 devdojo-streaming \u2014 Visual Studio Code<\/span><\/div>\n<div class=\"dd-vsc-tabs\">\n<label for=\"v1f1\" class=\"t1\"><b class=\"ic py\">PY<\/b>stream_basic.py<\/label>\n<label for=\"v1f2\" class=\"t2\"><b class=\"ic js\">JS<\/b>stream-basic.mjs<\/label>\n<label for=\"v1r1\" class=\"run run1\" title=\"Show the output in the terminal\">\u25b6 Run<\/label>\n<label for=\"v1r2\" class=\"run run2\" title=\"Show the output in the terminal\">\u25b6 Run<\/label>\n<\/div>\n<div class=\"dd-vsc-pane p1\">\n<div class=\"dd-vsc-crumbs\">devdojo-streaming \u203a stream_basic.py<\/div>\n<pre class=\"dd-vsc-code\"><code><span class=\"l\"><span class=\"c\"># pip install openai python-dotenv<\/span><\/span>\n<span class=\"l\"><span class=\"kn\">import<\/span> os<\/span>\n<span class=\"l\"><span class=\"kn\">from<\/span> dotenv <span class=\"kn\">import<\/span> <span class=\"n\">load_dotenv<\/span><\/span>\n<span class=\"l\"><span class=\"kn\">from<\/span> openai <span class=\"kn\">import<\/span> <span class=\"n\">OpenAI<\/span><\/span>\n<span class=\"l\"><\/span>\n<span class=\"l\"><span class=\"n\">load_dotenv<\/span>()<\/span>\n<span class=\"l\"><span class=\"n\">client<\/span> = <span class=\"n\">OpenAI<\/span>()  <span class=\"c\"># reads OPENAI_API_KEY from .env<\/span><\/span>\n<span class=\"l\"><span class=\"n\">model<\/span> = <span class=\"n\">os<\/span>.<span class=\"n\">getenv<\/span>(<span class=\"s\">\"OPENAI_MODEL\"<\/span>, <span class=\"s\">\"gpt-6-luna\"<\/span>)<\/span>\n<span class=\"l\"><\/span>\n<span class=\"l\"><span class=\"n\">stream<\/span> = <span class=\"n\">client<\/span>.<span class=\"n\">responses<\/span>.<span class=\"n\">create<\/span>(<\/span>\n<span class=\"l\">    <span class=\"n\">model<\/span>=<span class=\"n\">model<\/span>,<\/span>\n<span class=\"l\">    <span class=\"n\">instructions<\/span>=<span class=\"s\">\"You are a friendly coding teacher.\"<\/span>,<\/span>\n<span class=\"l\">    <span class=\"nb\">input<\/span>=<span class=\"s\">\"Explain streaming in one short sentence.\"<\/span>,<\/span>\n<span class=\"l\">    <span class=\"n\">stream<\/span>=<span class=\"kc\">True<\/span>,  <span class=\"c\"># &lt;- the only change from Part 3<\/span><\/span>\n<span class=\"l\">)<\/span>\n<span class=\"l\"><\/span>\n<span class=\"l\"><span class=\"k\">for<\/span> <span class=\"n\">event<\/span> <span class=\"k\">in<\/span> <span class=\"n\">stream<\/span>:<\/span>\n<span class=\"l\">    <span class=\"k\">if<\/span> <span class=\"n\">event<\/span>.<span class=\"n\">type<\/span> == <span class=\"s\">\"response.output_text.delta\"<\/span>:<\/span>\n<span class=\"l\">        <span class=\"nb\">print<\/span>(<span class=\"n\">event<\/span>.<span class=\"n\">delta<\/span>, <span class=\"n\">end<\/span>=<span class=\"s\">\"\"<\/span>, <span class=\"n\">flush<\/span>=<span class=\"kc\">True<\/span>)  <span class=\"c\"># print each piece now<\/span><\/span>\n<span class=\"l\">    <span class=\"k\">elif<\/span> <span class=\"n\">event<\/span>.<span class=\"n\">type<\/span> == <span class=\"s\">\"response.completed\"<\/span>:<\/span>\n<span class=\"l\">        <span class=\"n\">usage<\/span> = <span class=\"n\">event<\/span>.<span class=\"n\">response<\/span>.<span class=\"n\">usage<\/span><\/span>\n<span class=\"l\">        <span class=\"nb\">print<\/span>(<span class=\"s\">f\"\\n[done] tokens out: {<\/span><span class=\"n\">usage<\/span>.<span class=\"n\">output_tokens<\/span><span class=\"s\">}\"<\/span>)<\/span><\/code><\/pre>\n<div class=\"dd-vsc-term\"><div class=\"dd-term-head\"><span>TERMINAL<\/span><span>OUTPUT<\/span><span>PROBLEMS<\/span><span class=\"dd-term-note\">Example output \u2013 the model&#8217;s wording and token count will differ<\/span><\/div><pre><code><span class=\"gp\">$<\/span> python stream_basic.py\nStreaming means the model sends its answer in small pieces as soon as they are ready.\n[done] tokens out: 18<\/code><\/pre><\/div>\n<div class=\"dd-vsc-status\"><span>\u2387 main<\/span><span>Ln 22, Col 1<\/span><span class=\"sp\"><\/span><span>Spaces: 4<\/span><span>UTF-8<\/span><span>LF<\/span><span>Python<\/span><\/div>\n<\/div>\n<div class=\"dd-vsc-pane p2\">\n<div class=\"dd-vsc-crumbs\">devdojo-streaming \u203a stream-basic.mjs<\/div>\n<pre class=\"dd-vsc-code\"><code><span class=\"l\"><span class=\"c\">\/\/ npm install openai<\/span><\/span>\n<span class=\"l\"><span class=\"c\">\/\/ run with: node --env-file=.env stream-basic.mjs<\/span><\/span>\n<span class=\"l\"><span class=\"k\">import<\/span> <span class=\"n\">OpenAI<\/span> <span class=\"k\">from<\/span> <span class=\"s\">\"openai\"<\/span>;<\/span>\n<span class=\"l\"><\/span>\n<span class=\"l\"><span class=\"kc\">const<\/span> <span class=\"n\">client<\/span> = <span class=\"k\">new<\/span> <span class=\"n\">OpenAI<\/span>(); <span class=\"c\">\/\/ reads OPENAI_API_KEY<\/span><\/span>\n<span class=\"l\"><span class=\"kc\">const<\/span> <span class=\"n\">model<\/span> = <span class=\"n\">process<\/span>.<span class=\"n\">env<\/span>.<span class=\"n\">OPENAI_MODEL<\/span> ?? <span class=\"s\">\"gpt-6-luna\"<\/span>;<\/span>\n<span class=\"l\"><\/span>\n<span class=\"l\"><span class=\"kc\">const<\/span> <span class=\"n\">stream<\/span> = <span class=\"k\">await<\/span> <span class=\"n\">client<\/span>.<span class=\"n\">responses<\/span>.<span class=\"n\">create<\/span>({<\/span>\n<span class=\"l\">  <span class=\"n\">model<\/span>,<\/span>\n<span class=\"l\">  <span class=\"n\">instructions<\/span>: <span class=\"s\">\"You are a friendly coding teacher.\"<\/span>,<\/span>\n<span class=\"l\">  <span class=\"n\">input<\/span>: <span class=\"s\">\"Explain streaming in one short sentence.\"<\/span>,<\/span>\n<span class=\"l\">  <span class=\"n\">stream<\/span>: <span class=\"kc\">true<\/span>, <span class=\"c\">\/\/ &lt;- the only change from Part 3<\/span><\/span>\n<span class=\"l\">});<\/span>\n<span class=\"l\"><\/span>\n<span class=\"l\"><span class=\"k\">for<\/span> <span class=\"k\">await<\/span> (<span class=\"kc\">const<\/span> <span class=\"n\">event<\/span> <span class=\"k\">of<\/span> <span class=\"n\">stream<\/span>) {<\/span>\n<span class=\"l\">  <span class=\"k\">if<\/span> (<span class=\"n\">event<\/span>.<span class=\"n\">type<\/span> === <span class=\"s\">\"response.output_text.delta\"<\/span>) {<\/span>\n<span class=\"l\">    <span class=\"n\">process<\/span>.<span class=\"n\">stdout<\/span>.<span class=\"n\">write<\/span>(<span class=\"n\">event<\/span>.<span class=\"n\">delta<\/span>); <span class=\"c\">\/\/ print each piece now<\/span><\/span>\n<span class=\"l\">  } <span class=\"k\">else<\/span> <span class=\"k\">if<\/span> (<span class=\"n\">event<\/span>.<span class=\"n\">type<\/span> === <span class=\"s\">\"response.completed\"<\/span>) {<\/span>\n<span class=\"l\">    <span class=\"n\">console<\/span>.<span class=\"n\">log<\/span>(<span class=\"s\">`\\n[done] tokens out: ${<\/span><span class=\"n\">event<\/span>.<span class=\"n\">response<\/span>.<span class=\"n\">usage<\/span>.<span class=\"n\">output_tokens<\/span><span class=\"s\">}`<\/span>);<\/span>\n<span class=\"l\">  }<\/span>\n<span class=\"l\">}<\/span><\/code><\/pre>\n<div class=\"dd-vsc-term\"><div class=\"dd-term-head\"><span>TERMINAL<\/span><span>OUTPUT<\/span><span>PROBLEMS<\/span><span class=\"dd-term-note\">Example output \u2013 the model&#8217;s wording and token count will differ<\/span><\/div><pre><code><span class=\"gp\">$<\/span> node --env-file=.env stream-basic.mjs\nStreaming means the model sends its answer in small pieces as soon as they are ready.\n[done] tokens out: 18<\/code><\/pre><\/div>\n<div class=\"dd-vsc-status\"><span>\u2387 main<\/span><span>Ln 21, Col 1<\/span><span class=\"sp\"><\/span><span>Spaces: 2<\/span><span>UTF-8<\/span><span>LF<\/span><span>JavaScript<\/span><\/div>\n<\/div>\n<\/div>\n<p class=\"dd-vsc-hint\">Switch between the <strong>Python<\/strong> and <strong>Node.js<\/strong> tabs and click <strong>\u25b6 Run<\/strong> to see what prints. In your terminal the sentence appears a few words at a time.<\/p>\n<p>Two small details make it look right in a terminal. In Python, <code>print(..., end=\"\", flush=True)<\/code> prints the piece without a new line and pushes it to the screen immediately. In Node.js, <code>process.stdout.write()<\/code> does the same. Without them the text would appear in big jumps or all at the end.<\/p>\n<div class=\"dd-note\"><strong>Note:<\/strong> the full list of event types (tool calls, refusals, errors and more) is in the <a href=\"https:\/\/developers.openai.com\/api\/docs\/guides\/streaming-responses\" target=\"_blank\" rel=\"noopener\">OpenAI streaming guide<\/a>. Other providers such as Anthropic and Gemini also stream; the idea is the same, only the event names differ.<\/div>\n<\/div>\n\n\n\n<div class=\"dd-post\">\n<h2 id=\"sse\">What travels over the wire: Server-Sent Events<\/h2>\n<p>SDKs hide the details, but it helps to see the raw stream once. LLM APIs use a simple web standard called <strong>Server-Sent Events (SSE)<\/strong>. The server keeps one HTTP response open and writes lines that start with <code>data:<\/code>, each followed by a blank line. Ollama speaks the same OpenAI-compatible format, so you can try it locally with <code>curl<\/code>:<\/p>\n<div class=\"dd-vsc\" role=\"group\" aria-label=\"Code editor: raw_stream.sh\">\n<input type=\"radio\" name=\"v2\" id=\"v2f1\" class=\"f1\" checked>\n<input type=\"checkbox\" id=\"v2r1\" class=\"r1\">\n<div class=\"dd-vsc-bar\"><span class=\"dd-dots\"><i><\/i><i><\/i><i><\/i><\/span><span class=\"dd-vsc-title\">raw_stream.sh \u2014 devdojo-streaming \u2014 Visual Studio Code<\/span><\/div>\n<div class=\"dd-vsc-tabs\">\n<label for=\"v2f1\" class=\"t1\"><b class=\"ic sh\">$_<\/b>raw_stream.sh<\/label>\n<label for=\"v2r1\" class=\"run run1\" title=\"Show the output in the terminal\">\u25b6 Run<\/label>\n<\/div>\n<div class=\"dd-vsc-pane p1\">\n<div class=\"dd-vsc-crumbs\">devdojo-streaming \u203a raw_stream.sh<\/div>\n<pre class=\"dd-vsc-code\"><code><span class=\"l\"><span class=\"c\"># -N turns off curl's buffering so you see chunks as they arrive<\/span><\/span>\n<span class=\"l\">curl -N http:\/\/localhost:11434\/v1\/chat\/completions <span class=\"s\">\\<\/span><\/span>\n<span class=\"l\">  -H <span class=\"s\">\"Content-Type: application\/json\"<\/span> <span class=\"s\">\\<\/span><\/span>\n<span class=\"l\">  -d <span class=\"s\">'{\"model\":\"llama3.2\",\"stream\":true,<\/span><\/span>\n<span class=\"l\"><span class=\"s\">       \"messages\":[{\"role\":\"user\",\"content\":\"Say hi\"}]}'<\/span><\/span><\/code><\/pre>\n<div class=\"dd-vsc-term\"><div class=\"dd-term-head\"><span>TERMINAL<\/span><span>OUTPUT<\/span><span>PROBLEMS<\/span><span class=\"dd-term-note\">Example output \u2013 some fields trimmed, ids replaced with \u2026<\/span><\/div><pre><code><span class=\"gp\">$<\/span> bash raw_stream.sh\ndata: {\"id\":\"chatcmpl-\u2026\",\"object\":\"chat.completion.chunk\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"Hi\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-\u2026\",\"object\":\"chat.completion.chunk\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\" there\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-\u2026\",\"object\":\"chat.completion.chunk\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"!\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-\u2026\",\"object\":\"chat.completion.chunk\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"\"},\"finish_reason\":\"stop\"}]}\n\ndata: [DONE]<\/code><\/pre><\/div>\n<div class=\"dd-vsc-status\"><span>\u2387 main<\/span><span>Ln 5, Col 1<\/span><span class=\"sp\"><\/span><span>Spaces: 4<\/span><span>UTF-8<\/span><span>LF<\/span><span>Shell Script<\/span><\/div>\n<\/div>\n<\/div>\n<p class=\"dd-vsc-hint\">Click <strong>\u25b6 Run<\/strong> to see the raw stream. Every <code>data:<\/code> line is one small JSON chunk, and <code>[DONE]<\/code> marks the end.<\/p>\n<p>Each chunk carries a <code>delta<\/code>: only the <em>new<\/em> text, not the whole answer so far. Your job is to join the deltas together. The last chunk has a <code>finish_reason<\/code> such as <code>\"stop\"<\/code> (finished normally) or <code>\"length\"<\/code> (hit the token limit), and <code>[DONE]<\/code> closes the stream.<\/p>\n<p>The <code>openai<\/code> package gives you two styles of streaming. The newer <strong>Responses API<\/strong> (<code>client.responses.create<\/code>) sends named events, as you saw above. The older <strong>Chat Completions API<\/strong> (<code>client.chat.completions.create<\/code>) sends the <code>chat.completion.chunk<\/code> objects shown here. For the rest of this post we use Chat Completions, because Ollama, LM Studio, vLLM, Groq and many other providers support that exact format. One codebase then works with local and cloud models.<\/p>\n\n<h2 id=\"terminal-chat\">A terminal chatbot with memory<\/h2>\n<p>A chatbot is a loop: read the question, stream the answer, repeat. The only tricky part is <strong>memory<\/strong>. The model forgets everything between calls, so we keep a <code>messages<\/code> list and send the whole list every time. After the stream ends, we add the assistant&#8217;s full reply to the list.<\/p>\n<div class=\"dd-vsc\" role=\"group\" aria-label=\"Code editor: chat_cli.py\">\n<input type=\"radio\" name=\"v3\" id=\"v3f1\" class=\"f1\" checked>\n<input type=\"checkbox\" id=\"v3r1\" class=\"r1\">\n<div class=\"dd-vsc-bar\"><span class=\"dd-dots\"><i><\/i><i><\/i><i><\/i><\/span><span class=\"dd-vsc-title\">chat_cli.py \u2014 devdojo-streaming \u2014 Visual Studio Code<\/span><\/div>\n<div class=\"dd-vsc-tabs\">\n<label for=\"v3f1\" class=\"t1\"><b class=\"ic py\">PY<\/b>chat_cli.py<\/label>\n<label for=\"v3r1\" class=\"run run1\" title=\"Show the output in the terminal\">\u25b6 Run<\/label>\n<\/div>\n<div class=\"dd-vsc-pane p1\">\n<div class=\"dd-vsc-crumbs\">devdojo-streaming \u203a chat_cli.py<\/div>\n<pre class=\"dd-vsc-code\"><code><span class=\"l\"><span class=\"c\"># A terminal chatbot that streams answers and remembers the conversation.<\/span><\/span>\n<span class=\"l\"><span class=\"c\"># Works with Ollama (default) or any OpenAI-compatible API.<\/span><\/span>\n<span class=\"l\"><span class=\"kn\">import<\/span> os<\/span>\n<span class=\"l\"><span class=\"kn\">from<\/span> openai <span class=\"kn\">import<\/span> <span class=\"n\">OpenAI<\/span><\/span>\n<span class=\"l\"><\/span>\n<span class=\"l\"><span class=\"n\">client<\/span> = <span class=\"n\">OpenAI<\/span>(<\/span>\n<span class=\"l\">    <span class=\"n\">base_url<\/span>=<span class=\"n\">os<\/span>.<span class=\"n\">getenv<\/span>(<span class=\"s\">\"LLM_BASE_URL\"<\/span>, <span class=\"s\">\"http:\/\/localhost:11434\/v1\"<\/span>),<\/span>\n<span class=\"l\">    <span class=\"n\">api_key<\/span>=<span class=\"n\">os<\/span>.<span class=\"n\">getenv<\/span>(<span class=\"s\">\"LLM_API_KEY\"<\/span>, <span class=\"s\">\"ollama\"<\/span>),  <span class=\"c\"># Ollama ignores the key<\/span><\/span>\n<span class=\"l\">)<\/span>\n<span class=\"l\"><span class=\"n\">MODEL<\/span> = <span class=\"n\">os<\/span>.<span class=\"n\">getenv<\/span>(<span class=\"s\">\"LLM_MODEL\"<\/span>, <span class=\"s\">\"llama3.2\"<\/span>)<\/span>\n<span class=\"l\"><\/span>\n<span class=\"l\"><span class=\"n\">messages<\/span> = [{<span class=\"s\">\"role\"<\/span>: <span class=\"s\">\"system\"<\/span>, <span class=\"s\">\"content\"<\/span>: <span class=\"s\">\"You are a helpful, concise assistant.\"<\/span>}]<\/span>\n<span class=\"l\"><\/span>\n<span class=\"l\"><span class=\"k\">while<\/span> <span class=\"kc\">True<\/span>:<\/span>\n<span class=\"l\">    <span class=\"n\">user_text<\/span> = <span class=\"nb\">input<\/span>(<span class=\"s\">\"\\nYou: \"<\/span>).<span class=\"n\">strip<\/span>()<\/span>\n<span class=\"l\">    <span class=\"k\">if<\/span> <span class=\"n\">user_text<\/span>.<span class=\"n\">lower<\/span>() <span class=\"k\">in<\/span> {<span class=\"s\">\"exit\"<\/span>, <span class=\"s\">\"quit\"<\/span>}:<\/span>\n<span class=\"l\">        <span class=\"k\">break<\/span><\/span>\n<span class=\"l\">    <span class=\"n\">messages<\/span>.<span class=\"n\">append<\/span>({<span class=\"s\">\"role\"<\/span>: <span class=\"s\">\"user\"<\/span>, <span class=\"s\">\"content\"<\/span>: <span class=\"n\">user_text<\/span>})<\/span>\n<span class=\"l\"><\/span>\n<span class=\"l\">    <span class=\"nb\">print<\/span>(<span class=\"s\">\"AI: \"<\/span>, <span class=\"n\">end<\/span>=<span class=\"s\">\"\"<\/span>, <span class=\"n\">flush<\/span>=<span class=\"kc\">True<\/span>)<\/span>\n<span class=\"l\">    <span class=\"n\">reply<\/span> = <span class=\"s\">\"\"<\/span><\/span>\n<span class=\"l\">    <span class=\"n\">stream<\/span> = <span class=\"n\">client<\/span>.<span class=\"n\">chat<\/span>.<span class=\"n\">completions<\/span>.<span class=\"n\">create<\/span>(<\/span>\n<span class=\"l\">        <span class=\"n\">model<\/span>=<span class=\"n\">MODEL<\/span>, <span class=\"n\">messages<\/span>=<span class=\"n\">messages<\/span>, <span class=\"n\">stream<\/span>=<span class=\"kc\">True<\/span><\/span>\n<span class=\"l\">    )<\/span>\n<span class=\"l\">    <span class=\"k\">for<\/span> <span class=\"n\">chunk<\/span> <span class=\"k\">in<\/span> <span class=\"n\">stream<\/span>:<\/span>\n<span class=\"l\">        <span class=\"k\">if<\/span> <span class=\"k\">not<\/span> <span class=\"n\">chunk<\/span>.<span class=\"n\">choices<\/span>:<\/span>\n<span class=\"l\">            <span class=\"k\">continue<\/span><\/span>\n<span class=\"l\">        <span class=\"n\">piece<\/span> = <span class=\"n\">chunk<\/span>.<span class=\"n\">choices<\/span>[<span class=\"m\">0<\/span>].<span class=\"n\">delta<\/span>.<span class=\"n\">content<\/span> <span class=\"k\">or<\/span> <span class=\"s\">\"\"<\/span><\/span>\n<span class=\"l\">        <span class=\"n\">reply<\/span> += <span class=\"n\">piece<\/span><\/span>\n<span class=\"l\">        <span class=\"nb\">print<\/span>(<span class=\"n\">piece<\/span>, <span class=\"n\">end<\/span>=<span class=\"s\">\"\"<\/span>, <span class=\"n\">flush<\/span>=<span class=\"kc\">True<\/span>)<\/span>\n<span class=\"l\"><\/span>\n<span class=\"l\">    <span class=\"c\"># save the full answer so the next question has context<\/span><\/span>\n<span class=\"l\">    <span class=\"n\">messages<\/span>.<span class=\"n\">append<\/span>({<span class=\"s\">\"role\"<\/span>: <span class=\"s\">\"assistant\"<\/span>, <span class=\"s\">\"content\"<\/span>: <span class=\"n\">reply<\/span>})<\/span>\n<span class=\"l\">    <span class=\"nb\">print<\/span>()<\/span><\/code><\/pre>\n<div class=\"dd-vsc-term\"><div class=\"dd-term-head\"><span>TERMINAL<\/span><span>OUTPUT<\/span><span>PROBLEMS<\/span><span class=\"dd-term-note\">Example output \u2013 answers differ by model<\/span><\/div><pre><code><span class=\"gp\">$<\/span> python chat_cli.py\n\nYou: What is Node.js in one line?\nAI: Node.js is a runtime that lets you run JavaScript outside the browser.\n\nYou: And who made it?\nAI: Node.js was created by Ryan Dahl in 2009.\n\nYou: exit<\/code><\/pre><\/div>\n<div class=\"dd-vsc-status\"><span>\u2387 main<\/span><span>Ln 34, Col 1<\/span><span class=\"sp\"><\/span><span>Spaces: 4<\/span><span>UTF-8<\/span><span>LF<\/span><span>Python<\/span><\/div>\n<\/div>\n<\/div>\n<p class=\"dd-vsc-hint\">Click <strong>\u25b6 Run<\/strong>. Notice that the second question (<em>\u201cAnd who made it?\u201d<\/em>) works only because the history is sent again.<\/p>\n<p>By default this talks to Ollama on your own computer (<code>http:\/\/localhost:11434\/v1<\/code>). To use OpenAI instead, set <code>LLM_BASE_URL<\/code>, <code>LLM_API_KEY<\/code> and <code>LLM_MODEL<\/code> as shown in the setup block. Nothing else changes. Also notice <code>if not chunk.choices: continue<\/code>: some providers send a final chunk with usage numbers and no choices, and this line stops it from crashing your loop.<\/p>\n<div class=\"dd-tip\"><strong>Tip:<\/strong> the history grows with every message, and every message costs input tokens on each call. In the web version we keep only the last 20 messages. In Part 15 of this series you will learn smarter ways to manage long conversations.<\/div>\n<\/div>\n\n\n\n<div class=\"dd-post\">\n<h2 id=\"chat-ui\">Build a simple chat UI in the browser<\/h2>\n<p>Now the fun part: a real chat page with bubbles that fill up word by word. We need two pieces. A tiny <strong>server<\/strong> that holds the API key, calls the LLM with streaming and writes each piece into the HTTP response. And a single <strong>HTML page<\/strong> that sends the conversation and reads the response as a stream.<\/p>\n<figure>\n<svg viewBox=\"0 0 900 360\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" role=\"img\" aria-label=\"Architecture of the chat app: the browser keeps a messages array and posts it to the server; the server adds a system message, calls the LLM with stream true and writes each text piece back to the browser, which appends it to the reply bubble\">\n<rect width=\"900\" height=\"360\" fill=\"#0b1020\"\/>\n<defs><marker id=\"ddar\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto\"><path d=\"M0 0L10 5L0 10z\" fill=\"#a99bff\"\/><\/marker><\/defs>\n<g font-family=\"Segoe UI,Arial,sans-serif\">\n<rect x=\"30\" y=\"60\" width=\"240\" height=\"250\" rx=\"16\" fill=\"#151b3b\" stroke=\"#61dafb\" stroke-width=\"2\"\/>\n<text x=\"150\" y=\"92\" text-anchor=\"middle\" font-size=\"19\" font-weight=\"800\" fill=\"#fff\">Browser<\/text>\n<text x=\"150\" y=\"114\" text-anchor=\"middle\" font-size=\"13\" fill=\"#aab4e8\">index.html<\/text>\n<rect x=\"50\" y=\"132\" width=\"200\" height=\"70\" rx=\"10\" fill=\"#0b1020\"\/>\n<text x=\"62\" y=\"155\" font-size=\"13\" fill=\"#9cdcfe\" font-family=\"Consolas,monospace\">messages = [<\/text>\n<text x=\"74\" y=\"174\" font-size=\"13\" fill=\"#ce9178\" font-family=\"Consolas,monospace\">{user}, {assistant},<\/text>\n<text x=\"74\" y=\"193\" font-size=\"13\" fill=\"#ce9178\" font-family=\"Consolas,monospace\">{user} ]<\/text>\n<text x=\"150\" y=\"232\" text-anchor=\"middle\" font-size=\"14\" fill=\"#e8ebff\">reader.read() in a loop<\/text>\n<text x=\"150\" y=\"254\" text-anchor=\"middle\" font-size=\"14\" fill=\"#e8ebff\">appends text to bubble<\/text>\n<text x=\"150\" y=\"290\" text-anchor=\"middle\" font-size=\"13\" fill=\"#89e051\">Stop = AbortController<\/text>\n<rect x=\"340\" y=\"60\" width=\"220\" height=\"250\" rx=\"16\" fill=\"#151b3b\" stroke=\"#7c5cff\" stroke-width=\"2\"\/>\n<text x=\"450\" y=\"92\" text-anchor=\"middle\" font-size=\"19\" font-weight=\"800\" fill=\"#fff\">Your server<\/text>\n<text x=\"450\" y=\"114\" text-anchor=\"middle\" font-size=\"13\" fill=\"#aab4e8\">server.mjs or main.py<\/text>\n<text x=\"450\" y=\"160\" text-anchor=\"middle\" font-size=\"14\" fill=\"#e8ebff\">1. add system message<\/text>\n<text x=\"450\" y=\"188\" text-anchor=\"middle\" font-size=\"14\" fill=\"#e8ebff\">2. keep last 20 messages<\/text>\n<text x=\"450\" y=\"216\" text-anchor=\"middle\" font-size=\"14\" fill=\"#e8ebff\">3. call LLM, stream: true<\/text>\n<text x=\"450\" y=\"244\" text-anchor=\"middle\" font-size=\"14\" fill=\"#e8ebff\">4. res.write(each piece)<\/text>\n<text x=\"450\" y=\"290\" text-anchor=\"middle\" font-size=\"13\" fill=\"#d7ba7d\">API key stays here<\/text>\n<rect x=\"630\" y=\"100\" width=\"240\" height=\"170\" rx=\"16\" fill=\"#151b3b\" stroke=\"#3178c6\" stroke-width=\"2\"\/>\n<text x=\"750\" y=\"140\" text-anchor=\"middle\" font-size=\"19\" font-weight=\"800\" fill=\"#fff\">LLM<\/text>\n<text x=\"750\" y=\"170\" text-anchor=\"middle\" font-size=\"14\" fill=\"#e8ebff\">OpenAI or Ollama<\/text>\n<text x=\"750\" y=\"198\" text-anchor=\"middle\" font-size=\"14\" fill=\"#e8ebff\">sends chunks<\/text>\n<text x=\"750\" y=\"222\" text-anchor=\"middle\" font-size=\"13\" fill=\"#ce9178\" font-family=\"Consolas,monospace\">data: {&#8220;delta&#8221;:\u2026}<\/text>\n<path d=\"M272 150 H336\" stroke=\"#a99bff\" stroke-width=\"2.5\" marker-end=\"url(#ddar)\"\/>\n<text x=\"304\" y=\"140\" text-anchor=\"middle\" font-size=\"12\" fill=\"#aab4e8\">POST<\/text>\n<path d=\"M338 240 H274\" stroke=\"#89e051\" stroke-width=\"2.5\" marker-end=\"url(#ddar)\" stroke-dasharray=\"6 5\"\/>\n<text x=\"304\" y=\"232\" text-anchor=\"middle\" font-size=\"12\" fill=\"#89e051\">text<\/text>\n<path d=\"M562 150 H626\" stroke=\"#a99bff\" stroke-width=\"2.5\" marker-end=\"url(#ddar)\"\/>\n<path d=\"M628 230 H564\" stroke=\"#89e051\" stroke-width=\"2.5\" marker-end=\"url(#ddar)\" stroke-dasharray=\"6 5\"\/>\n<text x=\"596\" y=\"222\" text-anchor=\"middle\" font-size=\"12\" fill=\"#89e051\">chunks<\/text>\n<text x=\"30\" y=\"40\" font-size=\"18\" font-weight=\"700\" fill=\"#fff\">How the chat app moves one answer<\/text>\n<text x=\"30\" y=\"340\" font-size=\"14\" fill=\"#aab4e8\">Solid arrows = request. Dashed arrows = streamed text flowing back, piece by piece.<\/text>\n<\/g>\n<\/svg>\n<figcaption>Figure 2: The browser owns the conversation history, the server owns the API key, and text flows back in small pieces.<\/figcaption>\n<\/figure>\n<div class=\"dd-warn\"><strong>Important:<\/strong> never call the OpenAI API directly from browser JavaScript with your real key. Anyone can open DevTools and copy it. The browser should only talk to <em>your<\/em> server.<\/div>\n<p>The server below uses only Node.js built-ins plus the <code>openai<\/code> package, so there is no framework to learn. Read the comments, then switch to the <code>public\/index.html<\/code> tab.<\/p>\n<div class=\"dd-vsc\" role=\"group\" aria-label=\"Code editor: server.mjs, public\/index.html\">\n<input type=\"radio\" name=\"v4\" id=\"v4f1\" class=\"f1\" checked>\n<input type=\"radio\" name=\"v4\" id=\"v4f2\" class=\"f2\">\n<input type=\"checkbox\" id=\"v4r1\" class=\"r1\">\n<div class=\"dd-vsc-bar\"><span class=\"dd-dots\"><i><\/i><i><\/i><i><\/i><\/span><span class=\"dd-vsc-title\">server.mjs \u2014 devdojo-chat \u2014 Visual Studio Code<\/span><\/div>\n<div class=\"dd-vsc-tabs\">\n<label for=\"v4f1\" class=\"t1\"><b class=\"ic js\">JS<\/b>server.mjs<\/label>\n<label for=\"v4f2\" class=\"t2\"><b class=\"ic html\">&lt;&gt;<\/b>public\/index.html<\/label>\n<label for=\"v4r1\" class=\"run run1\" title=\"Show the output in the terminal\">\u25b6 Run<\/label>\n<\/div>\n<div class=\"dd-vsc-pane p1\">\n<div class=\"dd-vsc-crumbs\">devdojo-chat \u203a server.mjs<\/div>\n<pre class=\"dd-vsc-code\"><code><span class=\"l\"><span class=\"c\">\/\/ Tiny chat server: node --env-file=.env server.mjs  -&gt;  http:\/\/localhost:3000<\/span><\/span>\n<span class=\"l\"><span class=\"k\">import<\/span> <span class=\"n\">http<\/span> <span class=\"k\">from<\/span> <span class=\"s\">\"node:http\"<\/span>;<\/span>\n<span class=\"l\"><span class=\"k\">import<\/span> { <span class=\"n\">readFile<\/span> } <span class=\"k\">from<\/span> <span class=\"s\">\"node:fs\/promises\"<\/span>;<\/span>\n<span class=\"l\"><span class=\"k\">import<\/span> <span class=\"n\">OpenAI<\/span> <span class=\"k\">from<\/span> <span class=\"s\">\"openai\"<\/span>;<\/span>\n<span class=\"l\"><\/span>\n<span class=\"l\"><span class=\"kc\">const<\/span> <span class=\"n\">client<\/span> = <span class=\"k\">new<\/span> <span class=\"n\">OpenAI<\/span>({<\/span>\n<span class=\"l\">  <span class=\"n\">baseURL<\/span>: <span class=\"n\">process<\/span>.<span class=\"n\">env<\/span>.<span class=\"n\">LLM_BASE_URL<\/span> ?? <span class=\"s\">\"http:\/\/localhost:11434\/v1\"<\/span>,<\/span>\n<span class=\"l\">  <span class=\"n\">apiKey<\/span>: <span class=\"n\">process<\/span>.<span class=\"n\">env<\/span>.<span class=\"n\">LLM_API_KEY<\/span> ?? <span class=\"s\">\"ollama\"<\/span>,<\/span>\n<span class=\"l\">});<\/span>\n<span class=\"l\"><span class=\"kc\">const<\/span> <span class=\"n\">MODEL<\/span> = <span class=\"n\">process<\/span>.<span class=\"n\">env<\/span>.<span class=\"n\">LLM_MODEL<\/span> ?? <span class=\"s\">\"llama3.2\"<\/span>;<\/span>\n<span class=\"l\"><span class=\"kc\">const<\/span> <span class=\"n\">MAX_MESSAGES<\/span> = <span class=\"m\">20<\/span>; <span class=\"c\">\/\/ keep the history (and the bill) small<\/span><\/span>\n<span class=\"l\"><\/span>\n<span class=\"l\"><span class=\"kc\">const<\/span> <span class=\"n\">server<\/span> = <span class=\"n\">http<\/span>.<span class=\"n\">createServer<\/span>(<span class=\"k\">async<\/span> (<span class=\"n\">req<\/span>, <span class=\"n\">res<\/span>) =&gt; {<\/span>\n<span class=\"l\">  <span class=\"k\">if<\/span> (<span class=\"n\">req<\/span>.<span class=\"n\">method<\/span> === <span class=\"s\">\"GET\"<\/span> &amp;&amp; <span class=\"n\">req<\/span>.<span class=\"n\">url<\/span> === <span class=\"s\">\"\/\"<\/span>) {<\/span>\n<span class=\"l\">    <span class=\"n\">res<\/span>.<span class=\"n\">writeHead<\/span>(<span class=\"m\">200<\/span>, { <span class=\"s\">\"Content-Type\"<\/span>: <span class=\"s\">\"text\/html; charset=utf-8\"<\/span> });<\/span>\n<span class=\"l\">    <span class=\"k\">return<\/span> <span class=\"n\">res<\/span>.<span class=\"n\">end<\/span>(<span class=\"k\">await<\/span> <span class=\"n\">readFile<\/span>(<span class=\"s\">\"public\/index.html\"<\/span>));<\/span>\n<span class=\"l\">  }<\/span>\n<span class=\"l\">  <span class=\"k\">if<\/span> (<span class=\"n\">req<\/span>.<span class=\"n\">method<\/span> === <span class=\"s\">\"POST\"<\/span> &amp;&amp; <span class=\"n\">req<\/span>.<span class=\"n\">url<\/span> === <span class=\"s\">\"\/api\/chat\"<\/span>) {<\/span>\n<span class=\"l\">    <span class=\"kc\">let<\/span> <span class=\"n\">body<\/span> = <span class=\"s\">\"\"<\/span>;<\/span>\n<span class=\"l\">    <span class=\"k\">for<\/span> <span class=\"k\">await<\/span> (<span class=\"kc\">const<\/span> <span class=\"n\">part<\/span> <span class=\"k\">of<\/span> <span class=\"n\">req<\/span>) <span class=\"n\">body<\/span> += <span class=\"n\">part<\/span>;<\/span>\n<span class=\"l\">    <span class=\"kc\">const<\/span> { <span class=\"n\">messages<\/span> = [] } = <span class=\"nb\">JSON<\/span>.<span class=\"n\">parse<\/span>(<span class=\"n\">body<\/span> || <span class=\"s\">\"{}\"<\/span>);<\/span>\n<span class=\"l\"><\/span>\n<span class=\"l\">    <span class=\"n\">res<\/span>.<span class=\"n\">writeHead<\/span>(<span class=\"m\">200<\/span>, {<\/span>\n<span class=\"l\">      <span class=\"s\">\"Content-Type\"<\/span>: <span class=\"s\">\"text\/plain; charset=utf-8\"<\/span>,<\/span>\n<span class=\"l\">      <span class=\"s\">\"Cache-Control\"<\/span>: <span class=\"s\">\"no-cache\"<\/span>,<\/span>\n<span class=\"l\">      <span class=\"s\">\"X-Accel-Buffering\"<\/span>: <span class=\"s\">\"no\"<\/span>, <span class=\"c\">\/\/ stop Nginx from buffering the stream<\/span><\/span>\n<span class=\"l\">    });<\/span>\n<span class=\"l\"><\/span>\n<span class=\"l\">    <span class=\"kc\">const<\/span> <span class=\"n\">controller<\/span> = <span class=\"k\">new<\/span> <span class=\"n\">AbortController<\/span>();<\/span>\n<span class=\"l\">    <span class=\"n\">res<\/span>.<span class=\"n\">on<\/span>(<span class=\"s\">\"close\"<\/span>, () =&gt; <span class=\"n\">controller<\/span>.<span class=\"n\">abort<\/span>()); <span class=\"c\">\/\/ user left or pressed Stop<\/span><\/span>\n<span class=\"l\"><\/span>\n<span class=\"l\">    <span class=\"k\">try<\/span> {<\/span>\n<span class=\"l\">      <span class=\"kc\">const<\/span> <span class=\"n\">stream<\/span> = <span class=\"k\">await<\/span> <span class=\"n\">client<\/span>.<span class=\"n\">chat<\/span>.<span class=\"n\">completions<\/span>.<span class=\"n\">create<\/span>(<\/span>\n<span class=\"l\">        {<\/span>\n<span class=\"l\">          <span class=\"n\">model<\/span>: <span class=\"n\">MODEL<\/span>,<\/span>\n<span class=\"l\">          <span class=\"n\">stream<\/span>: <span class=\"kc\">true<\/span>,<\/span>\n<span class=\"l\">          <span class=\"n\">messages<\/span>: [<\/span>\n<span class=\"l\">            { <span class=\"n\">role<\/span>: <span class=\"s\">\"system\"<\/span>, <span class=\"n\">content<\/span>: <span class=\"s\">\"You are a friendly coding teacher.\"<\/span> },<\/span>\n<span class=\"l\">            ...<span class=\"n\">messages<\/span>.<span class=\"n\">slice<\/span>(-<span class=\"n\">MAX_MESSAGES<\/span>),<\/span>\n<span class=\"l\">          ],<\/span>\n<span class=\"l\">        },<\/span>\n<span class=\"l\">        { <span class=\"n\">signal<\/span>: <span class=\"n\">controller<\/span>.<span class=\"n\">signal<\/span> }<\/span>\n<span class=\"l\">      );<\/span>\n<span class=\"l\">      <span class=\"k\">for<\/span> <span class=\"k\">await<\/span> (<span class=\"kc\">const<\/span> <span class=\"n\">chunk<\/span> <span class=\"k\">of<\/span> <span class=\"n\">stream<\/span>) {<\/span>\n<span class=\"l\">        <span class=\"n\">res<\/span>.<span class=\"n\">write<\/span>(<span class=\"n\">chunk<\/span>.<span class=\"n\">choices<\/span>[<span class=\"m\">0<\/span>]?.<span class=\"n\">delta<\/span>?.<span class=\"n\">content<\/span> ?? <span class=\"s\">\"\"<\/span>);<\/span>\n<span class=\"l\">      }<\/span>\n<span class=\"l\">    } <span class=\"k\">catch<\/span> (<span class=\"n\">err<\/span>) {<\/span>\n<span class=\"l\">      <span class=\"k\">if<\/span> (!<span class=\"n\">controller<\/span>.<span class=\"n\">signal<\/span>.<span class=\"n\">aborted<\/span>) <span class=\"n\">res<\/span>.<span class=\"n\">write<\/span>(<span class=\"s\">\"\\n[error] \"<\/span> + <span class=\"n\">err<\/span>.<span class=\"n\">message<\/span>);<\/span>\n<span class=\"l\">    }<\/span>\n<span class=\"l\">    <span class=\"k\">return<\/span> <span class=\"n\">res<\/span>.<span class=\"n\">end<\/span>();<\/span>\n<span class=\"l\">  }<\/span>\n<span class=\"l\">  <span class=\"n\">res<\/span>.<span class=\"n\">writeHead<\/span>(<span class=\"m\">404<\/span>).<span class=\"n\">end<\/span>(<span class=\"s\">\"Not found\"<\/span>);<\/span>\n<span class=\"l\">});<\/span>\n<span class=\"l\"><\/span>\n<span class=\"l\"><span class=\"n\">server<\/span>.<span class=\"n\">listen<\/span>(<span class=\"m\">3000<\/span>, () =&gt; <span class=\"n\">console<\/span>.<span class=\"n\">log<\/span>(<span class=\"s\">\"Chat UI on http:\/\/localhost:3000\"<\/span>));<\/span><\/code><\/pre>\n<div class=\"dd-vsc-term\"><div class=\"dd-term-head\"><span>TERMINAL<\/span><span>OUTPUT<\/span><span>PROBLEMS<\/span><\/div><pre><code><span class=\"gp\">$<\/span> node --env-file=.env server.mjs\nChat UI on http:\/\/localhost:3000<\/code><\/pre><\/div>\n<div class=\"dd-vsc-status\"><span>\u2387 main<\/span><span>Ln 55, Col 1<\/span><span class=\"sp\"><\/span><span>Spaces: 2<\/span><span>UTF-8<\/span><span>LF<\/span><span>JavaScript<\/span><\/div>\n<\/div>\n<div class=\"dd-vsc-pane p2\">\n<div class=\"dd-vsc-crumbs\">devdojo-chat \u203a public \u203a index.html<\/div>\n<pre class=\"dd-vsc-code\"><code><span class=\"l\"><span class=\"c\">&lt;!doctype html&gt;<\/span><\/span>\n<span class=\"l\">&lt;<span class=\"kc\">html<\/span> <span class=\"n\">lang<\/span>=<span class=\"s\">\"en\"<\/span>&gt;<\/span>\n<span class=\"l\">&lt;<span class=\"kc\">head<\/span>&gt;<\/span>\n<span class=\"l\">  &lt;<span class=\"kc\">meta<\/span> <span class=\"n\">charset<\/span>=<span class=\"s\">\"utf-8\"<\/span>&gt;<\/span>\n<span class=\"l\">  &lt;<span class=\"kc\">title<\/span>&gt;DevDojo Chat&lt;\/<span class=\"kc\">title<\/span>&gt;<\/span>\n<span class=\"l\">  &lt;<span class=\"kc\">style<\/span>&gt;<\/span>\n<span class=\"l\">    <span class=\"kc\">body<\/span> { <span class=\"k\">font-family<\/span>: <span class=\"n\">system-ui<\/span>; <span class=\"k\">max-width<\/span>: <span class=\"m\">640<\/span><span class=\"k\">px<\/span>; <span class=\"k\">margin<\/span>: <span class=\"m\">40<\/span><span class=\"k\">px<\/span> <span class=\"kc\">auto<\/span>; }<\/span>\n<span class=\"l\">    #log <span class=\"kc\">p<\/span> { <span class=\"k\">padding<\/span>: <span class=\"m\">10<\/span><span class=\"k\">px<\/span> <span class=\"m\">14<\/span><span class=\"k\">px<\/span>; <span class=\"k\">border-radius<\/span>: <span class=\"m\">12<\/span><span class=\"k\">px<\/span>; <span class=\"k\">white-space<\/span>: <span class=\"kc\">pre-wrap<\/span>; }<\/span>\n<span class=\"l\">    .<span class=\"nc\">user<\/span> { <span class=\"k\">background<\/span>: <span class=\"m\">#e8f0ff<\/span>; <span class=\"k\">margin-left<\/span>: <span class=\"m\">20<\/span><span class=\"k\">%<\/span>; }<\/span>\n<span class=\"l\">    .<span class=\"nc\">ai<\/span> { <span class=\"k\">background<\/span>: <span class=\"m\">#f2f2f2<\/span>; <span class=\"k\">margin-right<\/span>: <span class=\"m\">20<\/span><span class=\"k\">%<\/span>; }<\/span>\n<span class=\"l\">  &lt;\/<span class=\"kc\">style<\/span>&gt;<\/span>\n<span class=\"l\">&lt;\/<span class=\"kc\">head<\/span>&gt;<\/span>\n<span class=\"l\">&lt;<span class=\"kc\">body<\/span>&gt;<\/span>\n<span class=\"l\">  &lt;<span class=\"kc\">h1<\/span>&gt;DevDojo Chat&lt;\/<span class=\"kc\">h1<\/span>&gt;<\/span>\n<span class=\"l\">  &lt;<span class=\"kc\">div<\/span> <span class=\"n\">id<\/span>=<span class=\"s\">\"log\"<\/span>&gt;&lt;\/<span class=\"kc\">div<\/span>&gt;<\/span>\n<span class=\"l\">  &lt;<span class=\"kc\">form<\/span> <span class=\"n\">id<\/span>=<span class=\"s\">\"form\"<\/span>&gt;<\/span>\n<span class=\"l\">    &lt;<span class=\"kc\">input<\/span> <span class=\"n\">id<\/span>=<span class=\"s\">\"text\"<\/span> <span class=\"n\">placeholder<\/span>=<span class=\"s\">\"Ask anything...\"<\/span> <span class=\"n\">autocomplete<\/span>=<span class=\"s\">\"off\"<\/span> <span class=\"n\">required<\/span>&gt;<\/span>\n<span class=\"l\">    &lt;<span class=\"kc\">button<\/span> <span class=\"n\">id<\/span>=<span class=\"s\">\"send\"<\/span>&gt;Send&lt;\/<span class=\"kc\">button<\/span>&gt;<\/span>\n<span class=\"l\">    &lt;<span class=\"kc\">button<\/span> <span class=\"n\">id<\/span>=<span class=\"s\">\"stop\"<\/span> <span class=\"n\">type<\/span>=<span class=\"s\">\"button\"<\/span> <span class=\"n\">disabled<\/span>&gt;Stop&lt;\/<span class=\"kc\">button<\/span>&gt;<\/span>\n<span class=\"l\">  &lt;\/<span class=\"kc\">form<\/span>&gt;<\/span>\n<span class=\"l\">  &lt;<span class=\"kc\">script<\/span>&gt;<\/span>\n<span class=\"l\">    <span class=\"kc\">const<\/span> <span class=\"n\">messages<\/span> = []; <span class=\"c\">\/\/ the whole conversation lives here<\/span><\/span>\n<span class=\"l\">    <span class=\"kc\">const<\/span> <span class=\"n\">log<\/span> = <span class=\"nb\">document<\/span>.<span class=\"n\">getElementById<\/span>(<span class=\"s\">\"log\"<\/span>);<\/span>\n<span class=\"l\">    <span class=\"kc\">let<\/span> <span class=\"n\">controller<\/span> = <span class=\"kc\">null<\/span>;<\/span>\n<span class=\"l\"><\/span>\n<span class=\"l\">    <span class=\"kc\">function<\/span> <span class=\"n\">addBubble<\/span>(<span class=\"n\">role<\/span>, <span class=\"n\">text<\/span>) {<\/span>\n<span class=\"l\">      <span class=\"kc\">const<\/span> <span class=\"n\">p<\/span> = <span class=\"nb\">document<\/span>.<span class=\"n\">createElement<\/span>(<span class=\"s\">\"p\"<\/span>);<\/span>\n<span class=\"l\">      <span class=\"n\">p<\/span>.<span class=\"n\">className<\/span> = <span class=\"n\">role<\/span> === <span class=\"s\">\"user\"<\/span> ? <span class=\"s\">\"user\"<\/span> : <span class=\"s\">\"ai\"<\/span>;<\/span>\n<span class=\"l\">      <span class=\"n\">p<\/span>.<span class=\"n\">textContent<\/span> = <span class=\"n\">text<\/span>;<\/span>\n<span class=\"l\">      <span class=\"n\">log<\/span>.<span class=\"n\">append<\/span>(<span class=\"n\">p<\/span>);<\/span>\n<span class=\"l\">      <span class=\"k\">return<\/span> <span class=\"n\">p<\/span>;<\/span>\n<span class=\"l\">    }<\/span>\n<span class=\"l\"><\/span>\n<span class=\"l\">    <span class=\"nb\">document<\/span>.<span class=\"n\">getElementById<\/span>(<span class=\"s\">\"form\"<\/span>).<span class=\"n\">onsubmit<\/span> = <span class=\"k\">async<\/span> (<span class=\"n\">e<\/span>) =&gt; {<\/span>\n<span class=\"l\">      <span class=\"n\">e<\/span>.<span class=\"n\">preventDefault<\/span>();<\/span>\n<span class=\"l\">      <span class=\"kc\">const<\/span> <span class=\"n\">input<\/span> = <span class=\"nb\">document<\/span>.<span class=\"n\">getElementById<\/span>(<span class=\"s\">\"text\"<\/span>);<\/span>\n<span class=\"l\">      <span class=\"n\">messages<\/span>.<span class=\"n\">push<\/span>({ <span class=\"n\">role<\/span>: <span class=\"s\">\"user\"<\/span>, <span class=\"n\">content<\/span>: <span class=\"n\">input<\/span>.<span class=\"n\">value<\/span> });<\/span>\n<span class=\"l\">      <span class=\"n\">addBubble<\/span>(<span class=\"s\">\"user\"<\/span>, <span class=\"n\">input<\/span>.<span class=\"n\">value<\/span>);<\/span>\n<span class=\"l\">      <span class=\"n\">input<\/span>.<span class=\"n\">value<\/span> = <span class=\"s\">\"\"<\/span>;<\/span>\n<span class=\"l\"><\/span>\n<span class=\"l\">      <span class=\"kc\">const<\/span> <span class=\"n\">bubble<\/span> = <span class=\"n\">addBubble<\/span>(<span class=\"s\">\"assistant\"<\/span>, <span class=\"s\">\"\"<\/span>);<\/span>\n<span class=\"l\">      <span class=\"n\">controller<\/span> = <span class=\"k\">new<\/span> <span class=\"n\">AbortController<\/span>();<\/span>\n<span class=\"l\">      <span class=\"n\">stop<\/span>.<span class=\"n\">disabled<\/span> = <span class=\"kc\">false<\/span>;<\/span>\n<span class=\"l\"><\/span>\n<span class=\"l\">      <span class=\"k\">try<\/span> {<\/span>\n<span class=\"l\">        <span class=\"kc\">const<\/span> <span class=\"n\">res<\/span> = <span class=\"k\">await<\/span> <span class=\"n\">fetch<\/span>(<span class=\"s\">\"\/api\/chat\"<\/span>, {<\/span>\n<span class=\"l\">          <span class=\"n\">method<\/span>: <span class=\"s\">\"POST\"<\/span>,<\/span>\n<span class=\"l\">          <span class=\"n\">headers<\/span>: { <span class=\"s\">\"Content-Type\"<\/span>: <span class=\"s\">\"application\/json\"<\/span> },<\/span>\n<span class=\"l\">          <span class=\"n\">body<\/span>: <span class=\"nb\">JSON<\/span>.<span class=\"n\">stringify<\/span>({ <span class=\"n\">messages<\/span> }),<\/span>\n<span class=\"l\">          <span class=\"n\">signal<\/span>: <span class=\"n\">controller<\/span>.<span class=\"n\">signal<\/span>,<\/span>\n<span class=\"l\">        });<\/span>\n<span class=\"l\">        <span class=\"kc\">const<\/span> <span class=\"n\">reader<\/span> = <span class=\"n\">res<\/span>.<span class=\"n\">body<\/span>.<span class=\"n\">getReader<\/span>();<\/span>\n<span class=\"l\">        <span class=\"kc\">const<\/span> <span class=\"n\">decoder<\/span> = <span class=\"k\">new<\/span> <span class=\"n\">TextDecoder<\/span>();<\/span>\n<span class=\"l\">        <span class=\"k\">while<\/span> (<span class=\"kc\">true<\/span>) {<\/span>\n<span class=\"l\">          <span class=\"kc\">const<\/span> { <span class=\"n\">done<\/span>, <span class=\"n\">value<\/span> } = <span class=\"k\">await<\/span> <span class=\"n\">reader<\/span>.<span class=\"n\">read<\/span>();<\/span>\n<span class=\"l\">          <span class=\"k\">if<\/span> (<span class=\"n\">done<\/span>) <span class=\"k\">break<\/span>;<\/span>\n<span class=\"l\">          <span class=\"n\">bubble<\/span>.<span class=\"n\">textContent<\/span> += <span class=\"n\">decoder<\/span>.<span class=\"n\">decode<\/span>(<span class=\"n\">value<\/span>, { <span class=\"n\">stream<\/span>: <span class=\"kc\">true<\/span> });<\/span>\n<span class=\"l\">        }<\/span>\n<span class=\"l\">      } <span class=\"k\">catch<\/span> (<span class=\"n\">err<\/span>) {<\/span>\n<span class=\"l\">        <span class=\"k\">if<\/span> (<span class=\"n\">err<\/span>.<span class=\"n\">name<\/span> !== <span class=\"s\">\"AbortError\"<\/span>) <span class=\"n\">bubble<\/span>.<span class=\"n\">textContent<\/span> += <span class=\"s\">\" [error]\"<\/span>;<\/span>\n<span class=\"l\">      }<\/span>\n<span class=\"l\">      <span class=\"n\">messages<\/span>.<span class=\"n\">push<\/span>({ <span class=\"n\">role<\/span>: <span class=\"s\">\"assistant\"<\/span>, <span class=\"n\">content<\/span>: <span class=\"n\">bubble<\/span>.<span class=\"n\">textContent<\/span> });<\/span>\n<span class=\"l\">      <span class=\"n\">stop<\/span>.<span class=\"n\">disabled<\/span> = <span class=\"kc\">true<\/span>;<\/span>\n<span class=\"l\">    };<\/span>\n<span class=\"l\"><\/span>\n<span class=\"l\">    <span class=\"kc\">const<\/span> <span class=\"n\">stop<\/span> = <span class=\"nb\">document<\/span>.<span class=\"n\">getElementById<\/span>(<span class=\"s\">\"stop\"<\/span>);<\/span>\n<span class=\"l\">    <span class=\"n\">stop<\/span>.<span class=\"n\">onclick<\/span> = () =&gt; <span class=\"n\">controller<\/span>?.<span class=\"n\">abort<\/span>(); <span class=\"c\">\/\/ cancel the answer<\/span><\/span>\n<span class=\"l\">  &lt;\/<span class=\"kc\">script<\/span>&gt;<\/span>\n<span class=\"l\">&lt;\/<span class=\"kc\">body<\/span>&gt;<\/span>\n<span class=\"l\">&lt;\/<span class=\"kc\">html<\/span>&gt;<\/span><\/code><\/pre>\n<div class=\"dd-vsc-status\"><span>\u2387 main<\/span><span>Ln 70, Col 1<\/span><span class=\"sp\"><\/span><span>Spaces: 2<\/span><span>UTF-8<\/span><span>LF<\/span><span>HTML<\/span><\/div>\n<\/div>\n<\/div>\n<p class=\"dd-vsc-hint\">Two files: click the <strong>server.mjs<\/strong> and <strong>public\/index.html<\/strong> tabs. Click <strong>\u25b6 Run<\/strong> on the server tab, then open <code>http:\/\/localhost:3000<\/code> in your browser.<\/p>\n<h3>How the browser reads the stream<\/h3>\n<p>The key lines are in the page&#8217;s <code>&lt;script&gt;<\/code>. A normal <code>fetch<\/code> call would use <code>await res.json()<\/code>, which waits for the whole body. Instead we call <code>res.body.getReader()<\/code>. Each <code>reader.read()<\/code> returns the next bytes as soon as they arrive, and <code>TextDecoder<\/code> turns bytes into text. The <code>{ stream: true }<\/code> option on <code>decode<\/code> matters: a Hindi or emoji character can be split across two chunks, and this option keeps the half-character until the rest arrives.<\/p>\n<p>The server sends <strong>plain text<\/strong> rather than SSE. That keeps the browser code tiny, because we do not need to parse <code>data:<\/code> lines. SSE is great when you need events with names (for example \u201ctool started\u201d), but for a basic chat, plain text streaming is enough. If you want to understand readable streams deeper, MDN has an excellent guide on <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/API\/Streams_API\/Using_readable_streams\" target=\"_blank\" rel=\"noopener\">using readable streams<\/a>.<\/p>\n<p>Open the step explorer below to follow one answer from the Send button to the screen.<\/p>\n<div class=\"dd-tabs\" role=\"group\" aria-label=\"Step explorer: the life of one streamed answer\">\n<input type=\"radio\" name=\"ddti\" id=\"ti1\" checked><input type=\"radio\" name=\"ddti\" id=\"ti2\"><input type=\"radio\" name=\"ddti\" id=\"ti3\"><input type=\"radio\" name=\"ddti\" id=\"ti4\">\n<div class=\"labels\"><label for=\"ti1\">1. Request<\/label><label for=\"ti2\">2. Model<\/label><label for=\"ti3\">3. Server<\/label><label for=\"ti4\">4. Browser<\/label><\/div>\n<div class=\"panel p1\"><p><strong>The browser sends the conversation.<\/strong> When you press Send, the page posts the full <code>messages<\/code> array (every earlier question and answer) to <code>\/api\/chat<\/code>. LLMs have no memory of their own, so history must travel with every request.<\/p><\/div>\n<div class=\"panel p2\"><p><strong>The model generates token by token.<\/strong> With <code>stream: true<\/code> the provider does not wait for the end. Each time it has a few new tokens it sends a small chunk over the same open HTTP connection, formatted as Server-Sent Events (<code>data: {\u2026}<\/code> lines).<\/p><\/div>\n<div class=\"panel p3\"><p><strong>Your server passes pieces along.<\/strong> The SDK turns those chunks into a loop. For every chunk the server pulls out <code>delta.content<\/code> and calls <code>res.write()<\/code> (Node.js) or <code>yield<\/code> (FastAPI). The API key never leaves the server.<\/p><\/div>\n<div class=\"panel p4\"><p><strong>The browser paints each piece.<\/strong> <code>res.body.getReader()<\/code> gives the page bytes as they arrive. <code>TextDecoder<\/code> turns them into text and the code appends it to the reply bubble. When the loop ends, the full reply is saved in <code>messages<\/code>.<\/p><\/div>\n<\/div>\n\n<h2 id=\"fastapi\">The same chat server in Python with FastAPI<\/h2>\n<p>Prefer Python? FastAPI has <code>StreamingResponse<\/code>, which takes a generator. Every <code>yield<\/code> sends one piece to the browser. We use <code>AsyncOpenAI<\/code> so the server can handle many chats at the same time without blocking. The HTML page stays exactly the same.<\/p>\n<div class=\"dd-vsc\" role=\"group\" aria-label=\"Code editor: main.py\">\n<input type=\"radio\" name=\"v5\" id=\"v5f1\" class=\"f1\" checked>\n<input type=\"checkbox\" id=\"v5r1\" class=\"r1\">\n<div class=\"dd-vsc-bar\"><span class=\"dd-dots\"><i><\/i><i><\/i><i><\/i><\/span><span class=\"dd-vsc-title\">main.py \u2014 devdojo-chat \u2014 Visual Studio Code<\/span><\/div>\n<div class=\"dd-vsc-tabs\">\n<label for=\"v5f1\" class=\"t1\"><b class=\"ic py\">PY<\/b>main.py<\/label>\n<label for=\"v5r1\" class=\"run run1\" title=\"Show the output in the terminal\">\u25b6 Run<\/label>\n<\/div>\n<div class=\"dd-vsc-pane p1\">\n<div class=\"dd-vsc-crumbs\">devdojo-chat \u203a main.py<\/div>\n<pre class=\"dd-vsc-code\"><code><span class=\"l\"><span class=\"c\"># pip install fastapi uvicorn openai<\/span><\/span>\n<span class=\"l\"><span class=\"c\"># run with: uvicorn main:app --reload<\/span><\/span>\n<span class=\"l\"><span class=\"kn\">import<\/span> os<\/span>\n<span class=\"l\"><span class=\"kn\">from<\/span> fastapi <span class=\"kn\">import<\/span> <span class=\"n\">FastAPI<\/span><\/span>\n<span class=\"l\"><span class=\"kn\">from<\/span> fastapi.responses <span class=\"kn\">import<\/span> <span class=\"n\">FileResponse<\/span>, <span class=\"n\">StreamingResponse<\/span><\/span>\n<span class=\"l\"><span class=\"kn\">from<\/span> openai <span class=\"kn\">import<\/span> <span class=\"n\">AsyncOpenAI<\/span><\/span>\n<span class=\"l\"><span class=\"kn\">from<\/span> pydantic <span class=\"kn\">import<\/span> <span class=\"n\">BaseModel<\/span><\/span>\n<span class=\"l\"><\/span>\n<span class=\"l\"><span class=\"n\">client<\/span> = <span class=\"n\">AsyncOpenAI<\/span>(<\/span>\n<span class=\"l\">    <span class=\"n\">base_url<\/span>=<span class=\"n\">os<\/span>.<span class=\"n\">getenv<\/span>(<span class=\"s\">\"LLM_BASE_URL\"<\/span>, <span class=\"s\">\"http:\/\/localhost:11434\/v1\"<\/span>),<\/span>\n<span class=\"l\">    <span class=\"n\">api_key<\/span>=<span class=\"n\">os<\/span>.<span class=\"n\">getenv<\/span>(<span class=\"s\">\"LLM_API_KEY\"<\/span>, <span class=\"s\">\"ollama\"<\/span>),<\/span>\n<span class=\"l\">)<\/span>\n<span class=\"l\"><span class=\"n\">MODEL<\/span> = <span class=\"n\">os<\/span>.<span class=\"n\">getenv<\/span>(<span class=\"s\">\"LLM_MODEL\"<\/span>, <span class=\"s\">\"llama3.2\"<\/span>)<\/span>\n<span class=\"l\"><span class=\"n\">app<\/span> = <span class=\"n\">FastAPI<\/span>()<\/span>\n<span class=\"l\"><\/span>\n<span class=\"l\"><\/span>\n<span class=\"l\"><span class=\"k\">class<\/span> <span class=\"nc\">ChatRequest<\/span>(<span class=\"n\">BaseModel<\/span>):<\/span>\n<span class=\"l\">    <span class=\"n\">messages<\/span>: <span class=\"nb\">list<\/span>[<span class=\"nb\">dict<\/span>]<\/span>\n<span class=\"l\"><\/span>\n<span class=\"l\"><\/span>\n<span class=\"l\"><span class=\"nd\">@app<\/span>.<span class=\"n\">get<\/span>(<span class=\"s\">\"\/\"<\/span>)<\/span>\n<span class=\"l\"><span class=\"k\">async<\/span> <span class=\"k\">def<\/span> <span class=\"nf\">home<\/span>():<\/span>\n<span class=\"l\">    <span class=\"k\">return<\/span> <span class=\"n\">FileResponse<\/span>(<span class=\"s\">\"public\/index.html\"<\/span>)  <span class=\"c\"># same page as the Node.js version<\/span><\/span>\n<span class=\"l\"><\/span>\n<span class=\"l\"><\/span>\n<span class=\"l\"><span class=\"nd\">@app<\/span>.<span class=\"n\">post<\/span>(<span class=\"s\">\"\/api\/chat\"<\/span>)<\/span>\n<span class=\"l\"><span class=\"k\">async<\/span> <span class=\"k\">def<\/span> <span class=\"nf\">chat<\/span>(<span class=\"n\">req<\/span>: <span class=\"n\">ChatRequest<\/span>):<\/span>\n<span class=\"l\">    <span class=\"k\">async<\/span> <span class=\"k\">def<\/span> <span class=\"nf\">generate<\/span>():<\/span>\n<span class=\"l\">        <span class=\"n\">stream<\/span> = <span class=\"k\">await<\/span> <span class=\"n\">client<\/span>.<span class=\"n\">chat<\/span>.<span class=\"n\">completions<\/span>.<span class=\"n\">create<\/span>(<\/span>\n<span class=\"l\">            <span class=\"n\">model<\/span>=<span class=\"n\">MODEL<\/span>,<\/span>\n<span class=\"l\">            <span class=\"n\">stream<\/span>=<span class=\"kc\">True<\/span>,<\/span>\n<span class=\"l\">            <span class=\"n\">messages<\/span>=[{<span class=\"s\">\"role\"<\/span>: <span class=\"s\">\"system\"<\/span>, <span class=\"s\">\"content\"<\/span>: <span class=\"s\">\"You are a friendly coding teacher.\"<\/span>}]<\/span>\n<span class=\"l\">            + <span class=\"n\">req<\/span>.<span class=\"n\">messages<\/span>[-<span class=\"m\">20<\/span>:],<\/span>\n<span class=\"l\">        )<\/span>\n<span class=\"l\">        <span class=\"k\">async<\/span> <span class=\"k\">for<\/span> <span class=\"n\">chunk<\/span> <span class=\"k\">in<\/span> <span class=\"n\">stream<\/span>:<\/span>\n<span class=\"l\">            <span class=\"k\">if<\/span> <span class=\"n\">chunk<\/span>.<span class=\"n\">choices<\/span> <span class=\"k\">and<\/span> <span class=\"n\">chunk<\/span>.<span class=\"n\">choices<\/span>[<span class=\"m\">0<\/span>].<span class=\"n\">delta<\/span>.<span class=\"n\">content<\/span>:<\/span>\n<span class=\"l\">                <span class=\"k\">yield<\/span> <span class=\"n\">chunk<\/span>.<span class=\"n\">choices<\/span>[<span class=\"m\">0<\/span>].<span class=\"n\">delta<\/span>.<span class=\"n\">content<\/span><\/span>\n<span class=\"l\"><\/span>\n<span class=\"l\">    <span class=\"k\">return<\/span> <span class=\"n\">StreamingResponse<\/span>(<\/span>\n<span class=\"l\">        <span class=\"n\">generate<\/span>(),<\/span>\n<span class=\"l\">        <span class=\"n\">media_type<\/span>=<span class=\"s\">\"text\/plain; charset=utf-8\"<\/span>,<\/span>\n<span class=\"l\">        <span class=\"n\">headers<\/span>={<span class=\"s\">\"Cache-Control\"<\/span>: <span class=\"s\">\"no-cache\"<\/span>, <span class=\"s\">\"X-Accel-Buffering\"<\/span>: <span class=\"s\">\"no\"<\/span>},<\/span>\n<span class=\"l\">    )<\/span><\/code><\/pre>\n<div class=\"dd-vsc-term\"><div class=\"dd-term-head\"><span>TERMINAL<\/span><span>OUTPUT<\/span><span>PROBLEMS<\/span><span class=\"dd-term-note\">Output from a test run \u2013 process id replaced with \u2026<\/span><\/div><pre><code><span class=\"gp\">$<\/span> uvicorn main:app --port 3000\nINFO:     Started server process [\u2026]\nINFO:     Waiting for application startup.\nINFO:     Application startup complete.\nINFO:     Uvicorn running on http:\/\/127.0.0.1:3000 (Press CTRL+C to quit)<\/code><\/pre><\/div>\n<div class=\"dd-vsc-status\"><span>\u2387 main<\/span><span>Ln 43, Col 1<\/span><span class=\"sp\"><\/span><span>Spaces: 4<\/span><span>UTF-8<\/span><span>LF<\/span><span>Python<\/span><\/div>\n<\/div>\n<\/div>\n<p class=\"dd-vsc-hint\">Click <strong>\u25b6 Run<\/strong>. The FastAPI server serves the exact same <code>public\/index.html<\/code>, so the browser code does not change.<\/p>\n<p>If you are new to FastAPI, our <a href=\"https:\/\/devdojo.co.in\/?p=203\">FastAPI tutorial<\/a> explains routes, Pydantic models and Uvicorn step by step.<\/p>\n\n<h2 id=\"stop-button\">Stopping an answer with a Stop button<\/h2>\n<p>Long answers cost money and time, so a Stop button is a must. Our page already has one. Here is the chain of events when you click it:<\/p>\n<div class=\"dd-steps\">\n<div><b>Browser<\/b><code>controller.abort()<\/code> cancels the <code>fetch<\/code>.<\/div>\n<div><b>Network<\/b>The HTTP connection to your server closes.<\/div>\n<div><b>Server<\/b><code>res.on(\"close\")<\/code> fires and aborts the LLM request.<\/div>\n<div><b>Provider<\/b>Generation stops, so you stop paying for new tokens.<\/div>\n<\/div>\n<p>The text that already arrived stays in the bubble and is still saved to <code>messages<\/code>, so the conversation keeps going naturally. In FastAPI, Starlette stops the generator when the client disconnects, which also closes the upstream stream.<\/p>\n<\/div>\n\n\n\n<div class=\"dd-post\">\n<h2 id=\"errors\">Common errors and fixes<\/h2>\n<div class=\"dd-tablewrap\"><table>\n<thead><tr><th>Problem<\/th><th>Why it happens<\/th><th>Fix<\/th><\/tr><\/thead>\n<tbody>\n<tr><td>Text appears all at once at the end<\/td><td>Output is buffered by Python&#8217;s <code>print<\/code>, a proxy (Nginx) or a compression middleware<\/td><td>Use <code>flush=True<\/code>; send <code>X-Accel-Buffering: no<\/code>; turn off gzip for the stream route<\/td><\/tr>\n<tr><td><code>TypeError: 'Stream' object is not subscriptable<\/code> \/ <code>.choices<\/code> is undefined<\/td><td>You treated a stream like a normal response<\/td><td>Loop over it with <code>for<\/code> \/ <code>for await<\/code>; read <code>delta<\/code>, not <code>message<\/code><\/td><\/tr>\n<tr><td><code>IndexError<\/code> on <code>chunk.choices[0]<\/code><\/td><td>Some providers send a final usage chunk with an empty <code>choices<\/code> list<\/td><td>Skip chunks without choices (<code>if not chunk.choices: continue<\/code> or <code>?.<\/code> in JS)<\/td><\/tr>\n<tr><td>The bot forgets the last question<\/td><td>Only the newest message was sent<\/td><td>Send the whole <code>messages<\/code> list and append the assistant reply after the stream ends<\/td><\/tr>\n<tr><td>Strange <code>\ufffd<\/code> characters in Hindi or emoji text<\/td><td>A multi-byte character was split across chunks<\/td><td>Use one <code>TextDecoder<\/code> and call <code>decode(value, { stream: true })<\/code><\/td><\/tr>\n<tr><td><code>JSON.parse<\/code> fails when reading SSE yourself<\/td><td>A network chunk can end in the middle of a <code>data:<\/code> line<\/td><td>Buffer text and only parse complete lines that end with a blank line, or use the SDK<\/td><\/tr>\n<tr><td><code>ECONNREFUSED 127.0.0.1:11434<\/code><\/td><td>Ollama is not running<\/td><td>Start the Ollama app or run <code>ollama serve<\/code>, then <code>ollama pull llama3.2<\/code><\/td><\/tr>\n<tr><td>401 Unauthorized<\/td><td>Wrong or missing API key for a cloud provider<\/td><td>Check <code>LLM_API_KEY<\/code> in <code>.env<\/code> and start Node.js with <code>--env-file=.env<\/code><\/td><\/tr>\n<\/tbody>\n<\/table><\/div>\n\n<h2 id=\"best-practices\">Best practices for streaming chat apps<\/h2>\n<ul>\n<li><strong>Keep secrets on the server.<\/strong> The browser talks to your API route; your route talks to the LLM.<\/li>\n<li><strong>Use <code>textContent<\/code>, not <code>innerHTML<\/code><\/strong>, when you show model output. Model text can contain HTML, and inserting it raw opens the door to XSS attacks.<\/li>\n<li><strong>Limit the history<\/strong> (for example the last 20 messages) and set <code>max_tokens<\/code> or <code>max_output_tokens<\/code> so one chat cannot burn your budget.<\/li>\n<li><strong>Always offer Stop<\/strong> and abort the upstream request when the client disconnects.<\/li>\n<li><strong>Show a typing hint<\/strong> (a blinking cursor or \u201c\u2026\u201d) until the first piece arrives, so users know something is happening.<\/li>\n<li><strong>Handle errors inside the stream.<\/strong> Once you have sent a 200 status you cannot change it, so write a short error message into the stream, as our server does.<\/li>\n<li><strong>Turn off buffering<\/strong> in proxies and hosting platforms, and test the deployed app, not just localhost.<\/li>\n<li><strong>Render Markdown later.<\/strong> Models often reply in Markdown. Once the basics work, render it with a safe Markdown library that sanitises HTML.<\/li>\n<\/ul>\n\n<h2 id=\"practice\">Practice task<\/h2>\n<div class=\"dd-note\">\n<p><strong>Upgrade your chat app.<\/strong> Using the code from this post:<\/p>\n<ol>\n<li>Add a <strong>\u201cNew chat\u201d<\/strong> button that clears the <code>messages<\/code> array and the screen.<\/li>\n<li>Show a blinking <strong>\u258d<\/strong> cursor at the end of the AI bubble while the answer is streaming, and remove it when the stream ends.<\/li>\n<li>Measure <strong>time to first token<\/strong>: record <code>performance.now()<\/code> before <code>fetch<\/code> and when the first piece arrives, and show it under the bubble (for example \u201cfirst words in 420 ms\u201d).<\/li>\n<li>Bonus: try the same question with a local Ollama model and a cloud model by changing only <code>.env<\/code>. Which one starts faster on your machine?<\/li>\n<\/ol>\n<\/div>\n\n<h2 id=\"faq\">Frequently asked questions<\/h2>\n<details><summary>Does streaming make the LLM answer faster?<\/summary><p>No, the total generation time is about the same. Streaming shows the first words much sooner, so the app feels faster and users can start reading immediately.<\/p><\/details>\n<details><summary>Does streaming cost more tokens or money?<\/summary><p>No. You pay for the same input and output tokens. You can even save money, because a Stop button ends generation early.<\/p><\/details>\n<details><summary>What is the difference between SSE and WebSockets?<\/summary><p>SSE is one-way: the server pushes text to the client over a normal HTTP response. WebSockets are two-way and need a separate protocol. LLM APIs use SSE because the answer only flows one way, and it works with ordinary HTTP tools.<\/p><\/details>\n<details><summary>Can I stream with Ollama and local models?<\/summary><p>Yes. Ollama streams by default on its own API and also supports <code>stream: true<\/code> on its OpenAI-compatible endpoint, which is why the same code works locally and in the cloud.<\/p><\/details>\n<details><summary>Why use fetch instead of the browser&#8217;s EventSource?<\/summary><p><code>EventSource<\/code> only supports GET requests, but a chat must POST the conversation history. <code>fetch<\/code> with <code>getReader()<\/code> supports POST, custom headers and cancelling with <code>AbortController<\/code>.<\/p><\/details>\n<details><summary>How do I stream in React or Next.js?<\/summary><p>The same idea works: keep the reply in state and append each piece as it arrives. We will build streaming UIs in the React and Next.js series, and libraries such as the Vercel AI SDK can handle the plumbing for you.<\/p><\/details>\n\n<h2 id=\"conclusion\">Conclusion<\/h2>\n<p>You now know how to stream LLM responses end to end: turn on <code>stream: true<\/code>, loop over events or chunks, join the deltas, and forward them from a small server to a browser that reads the response with <code>getReader()<\/code>. You built a terminal chatbot and a simple chat UI with memory and a Stop button, and the same code works with a free local model in Ollama or with a cloud model. This is the foundation of almost every AI chat product you use.<\/p>\n<p>Missed the previous part? Read <a href=\"https:\/\/devdojo.co.in\/?p=281\">Part 6: Run LLMs Locally with Ollama<\/a>. Want to build something bigger? Our <a href=\"https:\/\/devdojo.co.in\/?p=198\">RAG tutorial with Python and pgvector<\/a> shows how to chat with your own documents.<\/p>\n\n<div class=\"dd-next\"><strong>Next part \u2013 Generative AI Part 8: Structured Outputs: Getting Reliable JSON from LLMs.<\/strong> Chat is great for people, but programs need data. Next you will make models return valid JSON that matches a schema, validate it with Pydantic and Zod, and handle the cases where it still goes wrong.<\/div>\n\n<div class=\"dd-cta\">\n<h3>New posts every day on DevDojo<\/h3>\n<p>Follow the full <strong>Generative AI series<\/strong> \u2013 30 parts from &#8220;what is an LLM&#8221; to building a production AI assistant. Bookmark <a href=\"https:\/\/devdojo.co.in\/\">devdojo.co.in<\/a> and share a screenshot of your chat app in the comments.<\/p>\n<\/div>\n<\/div>\n\n","protected":false},"excerpt":{"rendered":"<p>Learn how to stream LLM responses in Python and Node.js, read Server-Sent Events and build a simple chat UI with memory and a Stop button for Ollama or OpenAI.<\/p>\n","protected":false},"author":1,"featured_media":283,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[38],"tags":[149,113,44,43,90,112,148],"class_list":["post-284","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-ai","tag-chat-ui","tag-generative-ai","tag-llm","tag-node-js","tag-openai-api","tag-series-generative-ai","tag-streaming"],"aioseo_notices":[],"aioseo_head":"\n\t\t<!-- All in One SEO 5.0.4 - aioseo.com -->\n\t<meta name=\"description\" content=\"Learn how to stream LLM responses in Python and Node.js, read Server-Sent Events and build a simple chat UI with memory and a Stop button for Ollama or OpenAI.\" \/>\n\t<meta name=\"robots\" content=\"max-image-preview:large\" \/>\n\t<meta name=\"author\" content=\"Lokendra\"\/>\n\t<link rel=\"canonical\" href=\"https:\/\/devdojo.co.in\/index.php\/2026\/11\/02\/genai-series-part-7-stream-llm-responses-chat-ui\/\" \/>\n\t\t<meta name=\"generator\" content=\"All in One SEO (AIOSEO) 5.0.4\" \/>\n\t\t<meta property=\"og:locale\" content=\"en_US\" \/>\n\t\t<meta property=\"og:site_name\" content=\"DevDojo - Empowering Developers\" \/>\n\t\t<meta property=\"og:type\" content=\"article\" \/>\n\t\t<meta property=\"og:title\" content=\"How to Stream LLM Responses and Build a Chat UI\" \/>\n\t\t<meta property=\"og:description\" content=\"Learn how to stream LLM responses in Python and Node.js, read Server-Sent Events and build a simple chat UI with memory and a Stop button for Ollama or OpenAI.\" \/>\n\t\t<meta property=\"og:url\" content=\"https:\/\/devdojo.co.in\/index.php\/2026\/11\/02\/genai-series-part-7-stream-llm-responses-chat-ui\/\" \/>\n\t\t<meta property=\"article:published_time\" content=\"2026-11-02T16:34:10+00:00\" \/>\n\t\t<meta property=\"article:modified_time\" content=\"2026-11-02T16:34:10+00:00\" \/>\n\t\t<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n\t\t<meta name=\"twitter:title\" content=\"How to Stream LLM Responses and Build a Chat UI\" \/>\n\t\t<meta name=\"twitter:description\" content=\"Learn how to stream LLM responses in Python and Node.js, read Server-Sent Events and build a simple chat UI with memory and a Stop button for Ollama or OpenAI.\" \/>\n\t\t<script type=\"application\/ld+json\" class=\"aioseo-schema\">\n\t\t\t{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"BlogPosting\",\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/2026\\\/11\\\/02\\\/genai-series-part-7-stream-llm-responses-chat-ui\\\/#blogposting\",\"name\":\"How to Stream LLM Responses and Build a Chat UI | DevDojo\",\"headline\":\"Generative AI Part 7: How to Stream LLM Responses and Build a Simple Chat UI\",\"author\":{\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/author\\\/lokendra\\\/#author\"},\"publisher\":{\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/#organization\"},\"image\":{\"@type\":\"ImageObject\",\"url\":\"https:\\\/\\\/devdojo.co.in\\\/wp-content\\\/uploads\\\/2026\\\/11\\\/genai-series-part-7-stream-llm-responses-chat-ui.jpg\",\"width\":1200,\"height\":630,\"caption\":\"Generative AI Part 7 banner: stream LLM responses from OpenAI or Ollama through a Node.js or FastAPI server into a browser chat UI\"},\"datePublished\":\"2026-11-02T16:34:10+00:00\",\"dateModified\":\"2026-11-02T16:34:10+00:00\",\"inLanguage\":\"en-US\",\"mainEntityOfPage\":{\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/2026\\\/11\\\/02\\\/genai-series-part-7-stream-llm-responses-chat-ui\\\/#webpage\"},\"isPartOf\":{\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/2026\\\/11\\\/02\\\/genai-series-part-7-stream-llm-responses-chat-ui\\\/#webpage\"},\"articleSection\":\"AI, Chat UI, Generative AI, LLM, Node.js, OpenAI API, Series: Generative AI, Streaming\"},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/2026\\\/11\\\/02\\\/genai-series-part-7-stream-llm-responses-chat-ui\\\/#breadcrumblist\",\"itemListElement\":[{\"@type\":\"ListItem\",\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/#listItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/devdojo.co.in\\\/\",\"nextItem\":{\"@type\":\"ListItem\",\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/category\\\/ai\\\/#listItem\",\"name\":\"AI\"}},{\"@type\":\"ListItem\",\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/category\\\/ai\\\/#listItem\",\"position\":2,\"name\":\"AI\",\"item\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/category\\\/ai\\\/\",\"nextItem\":{\"@type\":\"ListItem\",\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/2026\\\/11\\\/02\\\/genai-series-part-7-stream-llm-responses-chat-ui\\\/#listItem\",\"name\":\"Generative AI Part 7: How to Stream LLM Responses and Build a Simple Chat UI\"},\"previousItem\":{\"@type\":\"ListItem\",\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/#listItem\",\"name\":\"Home\"}},{\"@type\":\"ListItem\",\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/2026\\\/11\\\/02\\\/genai-series-part-7-stream-llm-responses-chat-ui\\\/#listItem\",\"position\":3,\"name\":\"Generative AI Part 7: How to Stream LLM Responses and Build a Simple Chat UI\",\"previousItem\":{\"@type\":\"ListItem\",\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/category\\\/ai\\\/#listItem\",\"name\":\"AI\"}}]},{\"@type\":\"Organization\",\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/#organization\",\"name\":\"DevDojo\",\"description\":\"Empowering Developers\",\"url\":\"https:\\\/\\\/devdojo.co.in\\\/\"},{\"@type\":\"Person\",\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/author\\\/lokendra\\\/#author\",\"url\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/author\\\/lokendra\\\/\",\"name\":\"Lokendra\",\"image\":{\"@type\":\"ImageObject\",\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/2026\\\/11\\\/02\\\/genai-series-part-7-stream-llm-responses-chat-ui\\\/#authorImage\",\"url\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/99f102f0cc9080237ce54b91304fd1b3b6fa29925cbaeba93cd796872e58b45c?s=96&d=mm&r=g\",\"width\":96,\"height\":96,\"caption\":\"Lokendra\"}},{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/2026\\\/11\\\/02\\\/genai-series-part-7-stream-llm-responses-chat-ui\\\/#webpage\",\"url\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/2026\\\/11\\\/02\\\/genai-series-part-7-stream-llm-responses-chat-ui\\\/\",\"name\":\"How to Stream LLM Responses and Build a Chat UI | DevDojo\",\"description\":\"Learn how to stream LLM responses in Python and Node.js, read Server-Sent Events and build a simple chat UI with memory and a Stop button for Ollama or OpenAI.\",\"inLanguage\":\"en-US\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/#website\"},\"breadcrumb\":{\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/2026\\\/11\\\/02\\\/genai-series-part-7-stream-llm-responses-chat-ui\\\/#breadcrumblist\"},\"author\":{\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/author\\\/lokendra\\\/#author\"},\"creator\":{\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/author\\\/lokendra\\\/#author\"},\"image\":{\"@type\":\"ImageObject\",\"url\":\"https:\\\/\\\/devdojo.co.in\\\/wp-content\\\/uploads\\\/2026\\\/11\\\/genai-series-part-7-stream-llm-responses-chat-ui.jpg\",\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/2026\\\/11\\\/02\\\/genai-series-part-7-stream-llm-responses-chat-ui\\\/#mainImage\",\"width\":1200,\"height\":630,\"caption\":\"Generative AI Part 7 banner: stream LLM responses from OpenAI or Ollama through a Node.js or FastAPI server into a browser chat UI\"},\"primaryImageOfPage\":{\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/2026\\\/11\\\/02\\\/genai-series-part-7-stream-llm-responses-chat-ui\\\/#mainImage\"},\"datePublished\":\"2026-11-02T16:34:10+00:00\",\"dateModified\":\"2026-11-02T16:34:10+00:00\"},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/#website\",\"url\":\"https:\\\/\\\/devdojo.co.in\\\/\",\"name\":\"DevDojo\",\"description\":\"Empowering Developers\",\"inLanguage\":\"en-US\",\"publisher\":{\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/#organization\"}}]}\n\t\t<\/script>\n\t\t<!-- All in One SEO -->\n\n","aioseo_head_json":{"title":"How to Stream LLM Responses and Build a Chat UI | DevDojo","description":"Learn how to stream LLM responses in Python and Node.js, read Server-Sent Events and build a simple chat UI with memory and a Stop button for Ollama or OpenAI.","canonical_url":"https:\/\/devdojo.co.in\/index.php\/2026\/11\/02\/genai-series-part-7-stream-llm-responses-chat-ui\/","robots":"max-image-preview:large","keywords":"","webmasterTools":{"miscellaneous":""},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"BlogPosting","@id":"https:\/\/devdojo.co.in\/index.php\/2026\/11\/02\/genai-series-part-7-stream-llm-responses-chat-ui\/#blogposting","name":"How to Stream LLM Responses and Build a Chat UI | DevDojo","headline":"Generative AI Part 7: How to Stream LLM Responses and Build a Simple Chat UI","author":{"@id":"https:\/\/devdojo.co.in\/index.php\/author\/lokendra\/#author"},"publisher":{"@id":"https:\/\/devdojo.co.in\/#organization"},"image":{"@type":"ImageObject","url":"https:\/\/devdojo.co.in\/wp-content\/uploads\/2026\/11\/genai-series-part-7-stream-llm-responses-chat-ui.jpg","width":1200,"height":630,"caption":"Generative AI Part 7 banner: stream LLM responses from OpenAI or Ollama through a Node.js or FastAPI server into a browser chat UI"},"datePublished":"2026-11-02T16:34:10+00:00","dateModified":"2026-11-02T16:34:10+00:00","inLanguage":"en-US","mainEntityOfPage":{"@id":"https:\/\/devdojo.co.in\/index.php\/2026\/11\/02\/genai-series-part-7-stream-llm-responses-chat-ui\/#webpage"},"isPartOf":{"@id":"https:\/\/devdojo.co.in\/index.php\/2026\/11\/02\/genai-series-part-7-stream-llm-responses-chat-ui\/#webpage"},"articleSection":"AI, Chat UI, Generative AI, LLM, Node.js, OpenAI API, Series: Generative AI, Streaming"},{"@type":"BreadcrumbList","@id":"https:\/\/devdojo.co.in\/index.php\/2026\/11\/02\/genai-series-part-7-stream-llm-responses-chat-ui\/#breadcrumblist","itemListElement":[{"@type":"ListItem","@id":"https:\/\/devdojo.co.in\/#listItem","position":1,"name":"Home","item":"https:\/\/devdojo.co.in\/","nextItem":{"@type":"ListItem","@id":"https:\/\/devdojo.co.in\/index.php\/category\/ai\/#listItem","name":"AI"}},{"@type":"ListItem","@id":"https:\/\/devdojo.co.in\/index.php\/category\/ai\/#listItem","position":2,"name":"AI","item":"https:\/\/devdojo.co.in\/index.php\/category\/ai\/","nextItem":{"@type":"ListItem","@id":"https:\/\/devdojo.co.in\/index.php\/2026\/11\/02\/genai-series-part-7-stream-llm-responses-chat-ui\/#listItem","name":"Generative AI Part 7: How to Stream LLM Responses and Build a Simple Chat UI"},"previousItem":{"@type":"ListItem","@id":"https:\/\/devdojo.co.in\/#listItem","name":"Home"}},{"@type":"ListItem","@id":"https:\/\/devdojo.co.in\/index.php\/2026\/11\/02\/genai-series-part-7-stream-llm-responses-chat-ui\/#listItem","position":3,"name":"Generative AI Part 7: How to Stream LLM Responses and Build a Simple Chat UI","previousItem":{"@type":"ListItem","@id":"https:\/\/devdojo.co.in\/index.php\/category\/ai\/#listItem","name":"AI"}}]},{"@type":"Organization","@id":"https:\/\/devdojo.co.in\/#organization","name":"DevDojo","description":"Empowering Developers","url":"https:\/\/devdojo.co.in\/"},{"@type":"Person","@id":"https:\/\/devdojo.co.in\/index.php\/author\/lokendra\/#author","url":"https:\/\/devdojo.co.in\/index.php\/author\/lokendra\/","name":"Lokendra","image":{"@type":"ImageObject","@id":"https:\/\/devdojo.co.in\/index.php\/2026\/11\/02\/genai-series-part-7-stream-llm-responses-chat-ui\/#authorImage","url":"https:\/\/secure.gravatar.com\/avatar\/99f102f0cc9080237ce54b91304fd1b3b6fa29925cbaeba93cd796872e58b45c?s=96&d=mm&r=g","width":96,"height":96,"caption":"Lokendra"}},{"@type":"WebPage","@id":"https:\/\/devdojo.co.in\/index.php\/2026\/11\/02\/genai-series-part-7-stream-llm-responses-chat-ui\/#webpage","url":"https:\/\/devdojo.co.in\/index.php\/2026\/11\/02\/genai-series-part-7-stream-llm-responses-chat-ui\/","name":"How to Stream LLM Responses and Build a Chat UI | DevDojo","description":"Learn how to stream LLM responses in Python and Node.js, read Server-Sent Events and build a simple chat UI with memory and a Stop button for Ollama or OpenAI.","inLanguage":"en-US","isPartOf":{"@id":"https:\/\/devdojo.co.in\/#website"},"breadcrumb":{"@id":"https:\/\/devdojo.co.in\/index.php\/2026\/11\/02\/genai-series-part-7-stream-llm-responses-chat-ui\/#breadcrumblist"},"author":{"@id":"https:\/\/devdojo.co.in\/index.php\/author\/lokendra\/#author"},"creator":{"@id":"https:\/\/devdojo.co.in\/index.php\/author\/lokendra\/#author"},"image":{"@type":"ImageObject","url":"https:\/\/devdojo.co.in\/wp-content\/uploads\/2026\/11\/genai-series-part-7-stream-llm-responses-chat-ui.jpg","@id":"https:\/\/devdojo.co.in\/index.php\/2026\/11\/02\/genai-series-part-7-stream-llm-responses-chat-ui\/#mainImage","width":1200,"height":630,"caption":"Generative AI Part 7 banner: stream LLM responses from OpenAI or Ollama through a Node.js or FastAPI server into a browser chat UI"},"primaryImageOfPage":{"@id":"https:\/\/devdojo.co.in\/index.php\/2026\/11\/02\/genai-series-part-7-stream-llm-responses-chat-ui\/#mainImage"},"datePublished":"2026-11-02T16:34:10+00:00","dateModified":"2026-11-02T16:34:10+00:00"},{"@type":"WebSite","@id":"https:\/\/devdojo.co.in\/#website","url":"https:\/\/devdojo.co.in\/","name":"DevDojo","description":"Empowering Developers","inLanguage":"en-US","publisher":{"@id":"https:\/\/devdojo.co.in\/#organization"}}]},"og:locale":"en_US","og:site_name":"DevDojo - Empowering Developers","og:type":"article","og:title":"How to Stream LLM Responses and Build a Chat UI","og:description":"Learn how to stream LLM responses in Python and Node.js, read Server-Sent Events and build a simple chat UI with memory and a Stop button for Ollama or OpenAI.","og:url":"https:\/\/devdojo.co.in\/index.php\/2026\/11\/02\/genai-series-part-7-stream-llm-responses-chat-ui\/","article:published_time":"2026-11-02T16:34:10+00:00","article:modified_time":"2026-11-02T16:34:10+00:00","twitter:card":"summary_large_image","twitter:title":"How to Stream LLM Responses and Build a Chat UI","twitter:description":"Learn how to stream LLM responses in Python and Node.js, read Server-Sent Events and build a simple chat UI with memory and a Stop button for Ollama or OpenAI."},"aioseo_meta_data":{"post_id":"284","title":"How to Stream LLM Responses and Build a Chat UI | DevDojo","description":"Learn how to stream LLM responses in Python and Node.js, read Server-Sent Events and build a simple chat UI with memory and a Stop button for Ollama or OpenAI.","keywords":null,"keyphrases":{"focus":{"keyphrase":"stream LLM responses"},"additional":[]},"primary_term":null,"canonical_url":null,"og_title":"How to Stream LLM Responses and Build a Chat UI","og_description":"Learn how to stream LLM responses in Python and Node.js, read Server-Sent Events and build a simple chat UI with memory and a Stop button for Ollama or OpenAI.","og_object_type":"default","og_image_type":"default","og_image_url":null,"og_image_width":null,"og_image_height":null,"og_image_custom_url":null,"og_image_custom_fields":null,"og_video":"","og_custom_url":null,"og_article_section":null,"og_article_tags":null,"twitter_use_og":false,"twitter_card":"default","twitter_image_type":"default","twitter_image_url":null,"twitter_image_custom_url":null,"twitter_image_custom_fields":null,"twitter_title":null,"twitter_description":null,"schema":{"blockGraphs":[],"customGraphs":[],"default":{"data":{"Article":[],"Course":[],"Dataset":[],"FAQPage":[],"Movie":[],"Person":[],"Product":[],"ProductReview":[],"Car":[],"Recipe":[],"Service":[],"SoftwareApplication":[],"WebPage":[]},"graphName":"BlogPosting","isEnabled":true},"graphs":[]},"schema_type":"default","schema_type_options":null,"pillar_content":false,"robots_default":true,"robots_noindex":false,"robots_noarchive":false,"robots_nosnippet":false,"robots_nofollow":false,"robots_noimageindex":false,"robots_noodp":false,"robots_notranslate":false,"robots_max_snippet":"-1","robots_max_videopreview":"-1","robots_max_imagepreview":"large","priority":0,"frequency":"default","local_seo":null,"breadcrumb_settings":null,"limit_modified_date":false,"ai":{"faqs":[],"keyPoints":[],"schemas":[],"titles":[],"descriptions":[],"socialPosts":{"email":{"subject":"","preview":"","content":""},"linkedin":[],"twitter":[],"facebook":[],"instagram":[]}},"created":"2026-11-02 16:11:01","updated":"2026-11-02 16:34:10","focus_keyword":"stream LLM responses","additional_keywords":null,"truseo_locale":null,"seo_analyzer_scan_date":null},"aioseo_breadcrumb":"<div class=\"aioseo-breadcrumbs\"><span class=\"aioseo-breadcrumb\">\n\t\t\t<a href=\"https:\/\/devdojo.co.in\/\" title=\"Home\">Home<\/a>\n\t\t<\/span><span class=\"aioseo-breadcrumb-separator\">\u00bb<\/span><span class=\"aioseo-breadcrumb\">\n\t\t\t<a href=\"https:\/\/devdojo.co.in\/index.php\/category\/ai\/\" title=\"AI\">AI<\/a>\n\t\t<\/span><span class=\"aioseo-breadcrumb-separator\">\u00bb<\/span><span class=\"aioseo-breadcrumb\">\n\t\t\tGenerative AI Part 7: How to Stream LLM Responses and Build a Simple Chat UI\n\t\t<\/span><\/div>","aioseo_breadcrumb_json":[{"label":"Home","link":"https:\/\/devdojo.co.in\/"},{"label":"AI","link":"https:\/\/devdojo.co.in\/index.php\/category\/ai\/"},{"label":"Generative AI Part 7: How to Stream LLM Responses and Build a Simple Chat UI","link":"https:\/\/devdojo.co.in\/index.php\/2026\/11\/02\/genai-series-part-7-stream-llm-responses-chat-ui\/"}],"_links":{"self":[{"href":"https:\/\/devdojo.co.in\/index.php\/wp-json\/wp\/v2\/posts\/284","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devdojo.co.in\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devdojo.co.in\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devdojo.co.in\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/devdojo.co.in\/index.php\/wp-json\/wp\/v2\/comments?post=284"}],"version-history":[{"count":1,"href":"https:\/\/devdojo.co.in\/index.php\/wp-json\/wp\/v2\/posts\/284\/revisions"}],"predecessor-version":[{"id":285,"href":"https:\/\/devdojo.co.in\/index.php\/wp-json\/wp\/v2\/posts\/284\/revisions\/285"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devdojo.co.in\/index.php\/wp-json\/wp\/v2\/media\/283"}],"wp:attachment":[{"href":"https:\/\/devdojo.co.in\/index.php\/wp-json\/wp\/v2\/media?parent=284"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devdojo.co.in\/index.php\/wp-json\/wp\/v2\/categories?post=284"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devdojo.co.in\/index.php\/wp-json\/wp\/v2\/tags?post=284"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}