|
1 | | -(ns eca.db) |
| 1 | +(ns eca.db |
| 2 | + (:require |
| 3 | + [babashka.fs :as fs] |
| 4 | + [clojure.java.io :as io] |
| 5 | + [clojure.string :as string] |
| 6 | + [cognitect.transit :as transit] |
| 7 | + [eca.config :as config :refer [get-env get-property]] |
| 8 | + [eca.logger :as logger] |
| 9 | + [eca.shared :as shared]) |
| 10 | + (:import |
| 11 | + [java.io OutputStream])) |
2 | 12 |
|
3 | 13 | (set! *warn-on-reflection* true) |
4 | 14 |
|
5 | | -(def ^:private one-million 1000000) |
| 15 | +(def ^:private logger-tag "[DB]") |
| 16 | + |
| 17 | +(def version 1) |
6 | 18 |
|
7 | 19 | (defonce initial-db |
8 | 20 | {:client-info {} |
|
11 | 23 | :chats {} |
12 | 24 | :chat-behaviors ["agent" "plan"] |
13 | 25 | :chat-default-behavior "agent" |
14 | | - :models {"o4-mini" {:tools true |
15 | | - :web-search false |
16 | | - :reason? true |
17 | | - :input-token-cost (/ 1.10 one-million) |
18 | | - :output-token-cost (/ 4.40 one-million)} |
19 | | - "o3" {:tools true |
20 | | - :web-search false |
21 | | - :reason? true |
22 | | - :input-token-cost (/ 2.0 one-million) |
23 | | - :output-token-cost (/ 8.0 one-million)} |
24 | | - "gpt-4.1" {:tools true |
25 | | - :web-search true |
26 | | - :reason? false |
27 | | - :max-output-tokens 32000 |
28 | | - :input-token-cost (/ 2.0 one-million) |
29 | | - :output-token-cost (/ 8.0 one-million)} |
30 | | - "gpt-5" {:tools true |
31 | | - :web-search true |
32 | | - :reason? true |
33 | | - :max-output-tokens 32000 |
34 | | - :input-token-cost (/ 1.25 one-million) |
35 | | - :output-token-cost (/ 10.0 one-million)} |
36 | | - "gpt-5-mini" {:tools true |
37 | | - :web-search true |
38 | | - :reason? true |
39 | | - :max-output-tokens 32000 |
40 | | - :input-token-cost (/ 0.25 one-million) |
41 | | - :output-token-cost (/ 2.0 one-million)} |
42 | | - "gpt-5-nano" {:tools true |
43 | | - :web-search true |
44 | | - :reason? true |
45 | | - :max-output-tokens 32000 |
46 | | - :input-token-cost (/ 0.05 one-million) |
47 | | - :output-token-cost (/ 0.4 one-million)} |
48 | | - "claude-sonnet-4-0" {:tools true |
49 | | - :web-search true |
50 | | - :max-output-tokens 8196 |
51 | | - :reason? true |
52 | | - :reason-tokens 2048 |
53 | | - :input-token-cost (/ 3.0 one-million) |
54 | | - :input-cache-creation-token-cost (/ 3.75 one-million) |
55 | | - :input-cache-read-token-cost (/ 0.3 one-million) |
56 | | - :output-token-cost (/ 15.0 one-million)} |
57 | | - "claude-opus-4-0" {:tools true |
58 | | - :web-search true |
59 | | - :max-output-tokens 8196 |
60 | | - :reason? true |
61 | | - :reason-tokens 2048 |
62 | | - :input-token-cost (/ 15.0 one-million) |
63 | | - :input-cache-creation-token-cost (/ 18.75 one-million) |
64 | | - :input-cache-read-token-cost (/ 1.5 one-million) |
65 | | - :output-token-cost (/ 75.0 one-million)} |
66 | | - "claude-opus-4-1" {:tools true |
67 | | - :web-search true |
68 | | - :max-output-tokens 8196 |
69 | | - :reason? true |
70 | | - :reason-tokens 2048 |
71 | | - :input-token-cost (/ 15.0 one-million) |
72 | | - :input-cache-creation-token-cost (/ 18.75 one-million) |
73 | | - :input-cache-read-token-cost (/ 1.5 one-million) |
74 | | - :output-token-cost (/ 75.0 one-million)} |
75 | | - "claude-3-5-haiku-latest" {:tools true |
76 | | - :web-search true |
77 | | - :reason? false |
78 | | - :max-output-tokens 4096 |
79 | | - :input-token-cost (/ 0.8 one-million) |
80 | | - :input-cache-creation-token-cost (/ 1.0 one-million) |
81 | | - :input-cache-read-token-cost (/ 0.08 one-million) |
82 | | - :output-token-cost (/ 4.0 one-million)}} ;; + ollama local models + custom provider models |
| 26 | + :models {} |
83 | 27 | :mcp-clients {}}) |
84 | 28 |
|
85 | 29 | (defonce db* (atom initial-db)) |
| 30 | + |
| 31 | +(defn ^:private no-flush-output-stream [^OutputStream os] |
| 32 | + (proxy [java.io.BufferedOutputStream] [os] |
| 33 | + (flush []) |
| 34 | + (close [] |
| 35 | + (let [^java.io.BufferedOutputStream this this] |
| 36 | + (proxy-super flush) |
| 37 | + (proxy-super close))))) |
| 38 | + |
| 39 | +(defn ^:private global-cache-dir [] |
| 40 | + (let [cache-home (or (get-env "XDG_CACHE_HOME") |
| 41 | + (io/file (get-property "user.home") ".cache"))] |
| 42 | + (io/file cache-home "eca"))) |
| 43 | + |
| 44 | +(defn ^:private workspaces-hash |
| 45 | + "Return an 8-char base64 (URL-safe, no padding) key for the given |
| 46 | + workspace set." |
| 47 | + [workspaces] |
| 48 | + (let [paths (->> workspaces |
| 49 | + (map #(str (fs/absolutize (fs/file (shared/uri->filename (:uri %)))))) |
| 50 | + (distinct) |
| 51 | + (sort)) |
| 52 | + joined (string/join ":" paths) |
| 53 | + md (java.security.MessageDigest/getInstance "SHA-256") |
| 54 | + digest (.digest (doto md (.update (.getBytes joined "UTF-8")))) |
| 55 | + encoder (-> (java.util.Base64/getUrlEncoder) |
| 56 | + (.withoutPadding)) |
| 57 | + key (.encodeToString encoder digest)] |
| 58 | + (subs key 0 (min 8 (count key))))) |
| 59 | + |
| 60 | +(defn ^:private transit-global-db-file [workspaces] |
| 61 | + (io/file (global-cache-dir) (workspaces-hash workspaces) "db.transit.json")) |
| 62 | + |
| 63 | +(defn ^:private read-cache [cache-file] |
| 64 | + (try |
| 65 | + (logger/logging-task |
| 66 | + :db/read-cache |
| 67 | + (if (fs/exists? cache-file) |
| 68 | + (let [cache (with-open [is (io/input-stream cache-file)] |
| 69 | + (transit/read (transit/reader is :json)))] |
| 70 | + (when (= version (:version cache)) |
| 71 | + cache)) |
| 72 | + (logger/info logger-tag (str "No existing DB cache found for " cache-file)))) |
| 73 | + (catch Throwable e |
| 74 | + (logger/error logger-tag "Could not load global cache from DB" e)))) |
| 75 | + |
| 76 | +(defn ^:private upsert-cache! [cache cache-file] |
| 77 | + (try |
| 78 | + (logger/logging-task |
| 79 | + :db/upsert-cache |
| 80 | + (io/make-parents cache-file) |
| 81 | + ;; https://github.com/cognitect/transit-clj/issues/43 |
| 82 | + (with-open [os ^OutputStream (no-flush-output-stream (io/output-stream cache-file))] |
| 83 | + (let [writer (transit/writer os :json)] |
| 84 | + (transit/write writer cache)))) |
| 85 | + (catch Throwable e |
| 86 | + (logger/error logger-tag (str "Could not upsert db cache to " cache-file) e)))) |
| 87 | + |
| 88 | +(defn ^:private read-workspaces-cache [workspaces] |
| 89 | + (let [cache (read-cache (transit-global-db-file workspaces))] |
| 90 | + (when (= version (:version cache)) |
| 91 | + cache))) |
| 92 | + |
| 93 | +(defn load-db-from-cache! [db*] |
| 94 | + (when-let [global-cache (read-workspaces-cache (:workspace-folders @db*))] |
| 95 | + (swap! db* (fn [state-db] |
| 96 | + (merge state-db |
| 97 | + (select-keys global-cache [:chats])))))) |
| 98 | + |
| 99 | +(defn update-workspaces-cache! [db] |
| 100 | + (-> (select-keys db [:chats]) |
| 101 | + (assoc :version version) |
| 102 | + (upsert-cache! (transit-global-db-file (:workspace-folders db))))) |
0 commit comments