Skip to content

Instantly share code, notes, and snippets.

@tommy-mor
Created October 11, 2025 05:18
Show Gist options
  • Save tommy-mor/7fcd3eeed28ea0c9fb5dbb864b048043 to your computer and use it in GitHub Desktop.
Save tommy-mor/7fcd3eeed28ea0c9fb5dbb864b048043 to your computer and use it in GitHub Desktop.
electric llm streaming
(ns electric-starter-app.llm
(:require [clj-http.client :as http]
[cheshire.core :as json]
[clojure.string :as str]))
(defn fetch-openrouter []
(let [api-key (System/getenv "OPENROUTER_API_KEY")
response (http/post "https://openrouter.ai/api/v1/chat/completions"
{:headers {"Authorization" (str "Bearer " api-key)
"Content-Type" "application/json"}
:body (json/generate-string
{:model "anthropic/claude-sonnet-4.5"
:messages [{:role "user" :content "Say hello in 5 words"}]})})]
(-> response :body (json/parse-string true) :choices first :message :content)))
(defn read-sse-line
"Read a line from a stream (until newline)"
[stream]
(let [sb (StringBuilder.)]
(loop []
(let [c (.read stream)]
(cond
(= c -1) nil ; EOF
(= c (int \newline)) (.toString sb)
:else (do (.append sb (char c))
(recur)))))))
(defn stream-openrouter
"Start streaming from OpenRouter. Returns an atom that accumulates the response.
The atom will be updated as chunks arrive."
[prompt]
(let [api-key (System/getenv "OPENROUTER_API_KEY")
!result (atom "")
!error (atom nil)]
(future
(try
(let [response (http/post "https://openrouter.ai/api/v1/chat/completions"
{:headers {"Authorization" (str "Bearer " api-key)
"Content-Type" "application/json"}
:as :stream
:body (json/generate-string
{:model "anthropic/claude-sonnet-4.5"
:messages [{:role "user" :content prompt}]
:stream true})})
stream (:body response)]
(loop []
(when-let [line (read-sse-line stream)]
(when-not (str/blank? line)
(when (str/starts-with? line "data: ")
(let [data (subs line 6)]
(when-not (= data "[DONE]")
(try
(let [parsed (json/parse-string data true)
content (-> parsed :choices first :delta :content)]
(when content
(swap! !result str content)))
(catch Exception e
(println "Parse error:" e "for line:" line)))))))
(recur)))
(.close stream))
(catch Exception e
(reset! !error (str "Error: " (.getMessage e)))
(println "Streaming error:" e))))
{:result !result
:error !error}))
(ns electric-starter-app.two-clocks
(:require [hyperfiddle.electric3 :as e]
[hyperfiddle.electric-dom3 :as dom]
#?(:clj [electric-starter-app.llm :as llm])))
(e/defn StreamingResponse []
(e/server
(let [{:keys [result error]} (llm/stream-openrouter "Tell me a short story about a robot in 2-3 sentences")
response (e/watch result)
err (e/watch error)]
(e/client
(dom/div
(dom/h3 (dom/text "🤖 Streaming AI Response:"))
(if err
(dom/p (dom/text "Error: " err))
(dom/p (dom/text (or response "Thinking...")))))))))
(e/defn TwoClocks []
(e/client
(let [s (e/server (e/System-time-ms))
c (e/client (e/System-time-ms))
ai-response (e/server (llm/fetch-openrouter))]
(dom/div (dom/text "server time: " s))
(dom/div (dom/text "client time: " c))
(dom/div (dom/text "skew: " (- s c)))
(dom/div (dom/text "AI says: " ai-response))
(dom/hr)
(StreamingResponse))))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment