Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/eca/config.clj
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
(def ^:private initial-config
{:openai-api-key nil
:anthropic-api-key nil
:google-gemini-api-key nil
:rules []
:ollama {:host "http://localhost"
:port 11434}
Expand Down
5 changes: 4 additions & 1 deletion src/eca/db.clj
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@
"gpt-4.1"
"claude-sonnet-4-0"
"claude-opus-4-0"
"claude-3-5-haiku-latest"] ;; + ollama local models
"claude-3-5-haiku-latest"
"gemini-2.5-pro"
"gemini-2.5-flash"
"gemini-2.5-flash-lite-preview-06-17"] ;; + ollama local models
:default-model "o4-mini" ;; unless a ollama model is running.
})

Expand Down
2 changes: 1 addition & 1 deletion src/eca/features/chat.clj
Original file line number Diff line number Diff line change
Expand Up @@ -172,4 +172,4 @@
all-subfiles-and-dirs)]
{:chat-id chat-id
:contexts (set/difference (set all-contexts)
(set contexts))}))
(set contexts))}))
12 changes: 12 additions & 0 deletions src/eca/llm_api.clj
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
[clojure.string :as string]
[eca.config :as config]
[eca.llm-providers.anthropic :as llm-providers.anthropic]
[eca.llm-providers.google :as llm-providers.google]
[eca.llm-providers.ollama :as llm-providers.ollama]
[eca.llm-providers.openai :as llm-providers.openai]))

Expand Down Expand Up @@ -40,6 +41,17 @@
:api-key (:anthropic-api-key config)}
{:on-message-received on-message-received
:on-error on-error})

(contains? #{"gemini-2.5-pro"
"gemini-2.5-flash"
"gemini-2.5-flash-lite-preview-06-17"} model)
(llm-providers.google/completion!
{:model model
:context context
:user-prompt user-prompt
:api-key (:google-gemini-api-key config)}
{:on-message-received on-message-received
:on-error on-error})

(string/starts-with? model config/ollama-model-prefix)
(llm-providers.ollama/completion!
Expand Down
73 changes: 73 additions & 0 deletions src/eca/llm_providers/google.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
(ns eca.llm-providers.google
(:require
[cheshire.core :as json]
[clojure.string :as str]
[clojure.java.io :as io]
[eca.llm-util :as llm-util]
[eca.logger :as logger]
[hato.client :as http]))

(set! *warn-on-reflection* true)

(def ^:private logger-tag "[GEMINI]")

(def ^:private gemini-url "https://generativelanguage.googleapis.com")
(def ^:private responses-path "/v1beta/models/$model:streamGenerateContent?alt=sse")

(defn ^:private url [path]
(format "%s%s"
(or (System/getenv "GEMINI_API_URL")
gemini-url)
path))


(defn ^:private base-completion-request! [{:keys [rid body api-key on-error on-response]}]
(let [api-key (or api-key
(System/getenv "GEMINI_API_URL"))
url (str/replace (url responses-path) "$model" (:model body))]
(logger/debug logger-tag (format "Sending input: '%s' instructions: '%s' tools: '%s' url: '%s'"
(:input body)
(:instructions body)
(:tools body)
url))
(http/post
url
{:headers {"x-goog-api-key" (str api-key)
"Content-Type" "application/json"}
:body (json/generate-string body)
:throw-exceptions? true
:async? true
:as :stream}
(fn [{:keys [status body]}]
(try
(if (not= 200 status)
(let [body-str (slurp body)]
(logger/warn logger-tag "Unexpected response status: %s body: %s" status body-str)
(on-error {:message (format "Gemini response status: %s body: %s" status body-str)}))
(with-open [rdr (io/reader body)]
(doseq [[event data] (llm-util/event-data-seq rdr)]
(llm-util/log-response logger-tag rid event data)
(on-response event data))))
(catch Exception e
(on-error {:exception e}))))
(fn [e]
(on-error {:exception e})))))

(defn completion! [{:keys [model user-prompt context temperature api-key past-messages tools web-search]
:or {temperature 1.0}}
{:keys [on-message-received on-error on-tool-called on-reason]}]
(let [input (conj past-messages {:role "user" :parts [{:text user-prompt}]})
tools (cond-> tools
web-search (conj {:google_search {}}))
body {:model model
:contents input
:system_instruction {:parts [{:text context}]}
:tools tools
:stream true}
on-response-fn (fn handle-response [_event data]
())]
(base-completion-request!
{:body body
:api-key api-key
:on-error on-error
:on-response on-response-fn})))