Created
October 11, 2025 05:18
-
-
Save tommy-mor/7fcd3eeed28ea0c9fb5dbb864b048043 to your computer and use it in GitHub Desktop.
electric llm streaming
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| (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})) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| (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